Reimplement compose and add tiling windows
This commit is contained in:
1
core/static/css/bulma-calendar.min.css
vendored
1
core/static/css/bulma-calendar.min.css
vendored
File diff suppressed because one or more lines are too long
1
core/static/css/bulma-slider.min.css
vendored
1
core/static/css/bulma-slider.min.css
vendored
File diff suppressed because one or more lines are too long
1
core/static/css/bulma-switch.min.css
vendored
1
core/static/css/bulma-switch.min.css
vendored
File diff suppressed because one or more lines are too long
1
core/static/css/bulma-tagsinput.min.css
vendored
1
core/static/css/bulma-tagsinput.min.css
vendored
@@ -1 +0,0 @@
|
||||
@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.tagsinput{height:auto!important}.tagsinput .control{margin-bottom:.1em!important;margin-top:.1em!important}.tagsinput input{border:none;margin-bottom:.1em!important;margin-top:.1em!important}.tagsinput .tag.is-active{background-color:#00d1b2;color:#fff}
|
||||
1
core/static/css/bulma-tooltip.min.css
vendored
1
core/static/css/bulma-tooltip.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,7 +1,8 @@
|
||||
.compose-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.compose-shell .compose-shell-head {
|
||||
@@ -22,13 +23,37 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.compose-shell .compose-context-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.compose-shell .compose-context-primary {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.compose-shell .compose-context-secondary {
|
||||
flex: 0 0 11rem;
|
||||
min-width: 9rem;
|
||||
}
|
||||
|
||||
.compose-shell .compose-contact-switch,
|
||||
.compose-shell .compose-platform-switch {
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.compose-shell .compose-contact-switch .select,
|
||||
.compose-shell .compose-platform-switch .select,
|
||||
.compose-shell .compose-contact-switch select,
|
||||
.compose-shell .compose-platform-switch select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compose-shell .compose-status {
|
||||
min-height: 1.25rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.compose-shell .compose-status .button {
|
||||
@@ -49,12 +74,32 @@
|
||||
color: var(--bulma-success, #257953);
|
||||
}
|
||||
|
||||
.gia-widget-control.gia-widget-control-no-scroll > .compose-shell {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gia-widget-control.gia-widget-control-no-scroll > .compose-shell .compose-shell-head,
|
||||
.gia-widget-control.gia-widget-control-no-scroll > .compose-shell .compose-status,
|
||||
.gia-widget-control.gia-widget-control-no-scroll > .compose-shell .compose-form {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.gia-widget-control.gia-widget-control-no-scroll > .compose-shell .compose-thread {
|
||||
flex: 1 1 80%;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.compose-shell .compose-thread {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
min-height: 24rem;
|
||||
max-height: 68vh;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--bulma-border, #dbdbdb);
|
||||
@@ -236,7 +281,7 @@
|
||||
.compose-shell .compose-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.compose-shell .compose-send-safety {
|
||||
@@ -271,21 +316,8 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.compose-shell .compose-composer-capsule {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--bulma-border, #dbdbdb);
|
||||
border-radius: 0.875rem;
|
||||
background: var(--bulma-scheme-main-bis, #f7f8fa);
|
||||
}
|
||||
|
||||
.compose-shell .compose-textarea {
|
||||
flex: 1 1 auto;
|
||||
min-height: 2.75rem;
|
||||
max-height: 8rem;
|
||||
resize: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.compose-shell .compose-send-btn {
|
||||
@@ -305,19 +337,23 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.compose-shell .compose-context-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.compose-shell .compose-context-secondary {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.compose-shell .compose-thread {
|
||||
max-height: 60vh;
|
||||
min-height: 18rem;
|
||||
}
|
||||
|
||||
.compose-shell .compose-bubble {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.compose-shell .compose-composer-capsule {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.compose-shell .compose-send-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,143 @@
|
||||
:root {
|
||||
--gia-navbar-height: 3.25rem;
|
||||
--gia-page-bg: #edf2f8;
|
||||
--gia-surface-1: rgba(255, 255, 255, 0.86);
|
||||
--gia-surface-2: #ffffff;
|
||||
--gia-surface-3: #f5f7fb;
|
||||
--gia-border: rgba(15, 23, 42, 0.12);
|
||||
--gia-border-strong: rgba(15, 23, 42, 0.2);
|
||||
--gia-text: #0f172a;
|
||||
--gia-text-muted: #526277;
|
||||
--gia-text-soft: #748399;
|
||||
--gia-hover: rgba(29, 78, 216, 0.08);
|
||||
--gia-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
||||
--gia-brand-surface: rgba(255, 255, 255, 0.92);
|
||||
--gia-brand-border: rgba(15, 23, 42, 0.08);
|
||||
--gia-brand-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
||||
--bulma-body-background-color: var(--gia-page-bg);
|
||||
--bulma-body-color: var(--gia-text);
|
||||
--bulma-background: var(--gia-page-bg);
|
||||
--bulma-text: var(--gia-text);
|
||||
--bulma-text-strong: var(--gia-text);
|
||||
--bulma-text-weak: var(--gia-text-muted);
|
||||
--bulma-border: var(--gia-border);
|
||||
--bulma-scheme-main: var(--gia-surface-2);
|
||||
--bulma-scheme-main-bis: var(--gia-surface-3);
|
||||
--bulma-scheme-main-ter: #e9eef6;
|
||||
--bulma-link: #1d4ed8;
|
||||
--bulma-link-light: #e8f0ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--gia-page-bg: #0b1220;
|
||||
--gia-surface-1: rgba(15, 23, 42, 0.9);
|
||||
--gia-surface-2: #111827;
|
||||
--gia-surface-3: #182235;
|
||||
--gia-border: rgba(148, 163, 184, 0.22);
|
||||
--gia-border-strong: rgba(148, 163, 184, 0.34);
|
||||
--gia-text: #e5edf8;
|
||||
--gia-text-muted: #b8c5d7;
|
||||
--gia-text-soft: #94a3b8;
|
||||
--gia-hover: rgba(148, 163, 184, 0.12);
|
||||
--gia-shadow: 0 12px 28px rgba(0, 0, 0, 0.34);
|
||||
--gia-brand-surface: rgba(17, 24, 39, 0.94);
|
||||
--gia-brand-border: rgba(148, 163, 184, 0.24);
|
||||
--gia-brand-shadow: 0 12px 28px rgba(0, 0, 0, 0.34);
|
||||
--bulma-body-background-color: var(--gia-page-bg);
|
||||
--bulma-body-color: var(--gia-text);
|
||||
--bulma-background: var(--gia-page-bg);
|
||||
--bulma-text: var(--gia-text);
|
||||
--bulma-text-strong: #f8fbff;
|
||||
--bulma-text-weak: var(--gia-text-muted);
|
||||
--bulma-border: var(--gia-border);
|
||||
--bulma-scheme-main: var(--gia-surface-2);
|
||||
--bulma-scheme-main-bis: var(--gia-surface-3);
|
||||
--bulma-scheme-main-ter: #1d293d;
|
||||
--bulma-link: #93c5fd;
|
||||
--bulma-link-light: rgba(59, 130, 246, 0.18);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
background-color: var(--gia-page-bg);
|
||||
color: var(--gia-text);
|
||||
}
|
||||
|
||||
body,
|
||||
body .title,
|
||||
body .subtitle,
|
||||
body .label,
|
||||
body .content,
|
||||
body .table,
|
||||
body .panel-heading,
|
||||
body .menu-label,
|
||||
body .modal-card-title,
|
||||
body .navbar-item,
|
||||
body .navbar-link {
|
||||
color: var(--gia-text);
|
||||
}
|
||||
|
||||
body .help,
|
||||
body .has-text-grey,
|
||||
body .has-text-grey-dark,
|
||||
body .has-text-grey-light {
|
||||
color: var(--gia-text-muted) !important;
|
||||
}
|
||||
|
||||
.box,
|
||||
.card,
|
||||
.panel,
|
||||
.dropdown-content,
|
||||
.modal-card,
|
||||
.modal-card-head,
|
||||
.modal-card-body,
|
||||
.modal-card-foot,
|
||||
.tabs a,
|
||||
.menu-list a,
|
||||
.pagination-link,
|
||||
.pagination-next,
|
||||
.pagination-previous,
|
||||
.button.is-light,
|
||||
.button.is-white {
|
||||
background-color: var(--gia-surface-2);
|
||||
border-color: var(--gia-border);
|
||||
color: var(--gia-text);
|
||||
}
|
||||
|
||||
.tabs.is-boxed a {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.modal-card-head,
|
||||
.modal-card-foot,
|
||||
.panel-heading {
|
||||
background-color: var(--gia-surface-3);
|
||||
}
|
||||
|
||||
.message.is-light .message-body,
|
||||
.table-container,
|
||||
.floating-window .panel {
|
||||
background-color: var(--gia-surface-2) !important;
|
||||
border-color: var(--gia-border) !important;
|
||||
color: var(--gia-text);
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea,
|
||||
.select select {
|
||||
background-color: var(--gia-surface-2);
|
||||
color: var(--gia-text);
|
||||
border-color: var(--gia-border-strong);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.input::placeholder,
|
||||
.textarea::placeholder {
|
||||
color: var(--gia-text-soft);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon { border-bottom: 0 !important; }
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
@@ -34,6 +174,7 @@
|
||||
|
||||
.table {
|
||||
background: transparent !important;
|
||||
color: var(--gia-text);
|
||||
}
|
||||
|
||||
tr {
|
||||
@@ -47,7 +188,7 @@ a.panel-block {
|
||||
tr:hover,
|
||||
a.panel-block:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(221, 224, 255, 0.3) !important;
|
||||
background-color: var(--gia-hover) !important;
|
||||
}
|
||||
|
||||
.has-background-grey-lighter {
|
||||
@@ -55,7 +196,9 @@ a.panel-block:hover {
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: rgba(0, 0, 0, 0.03) !important;
|
||||
background-color: var(--gia-surface-1) !important;
|
||||
border-bottom: 1px solid var(--gia-border);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.gia-brand-shell {
|
||||
@@ -69,26 +212,30 @@ a.panel-block:hover {
|
||||
justify-content: center;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(21, 28, 39, 0.08);
|
||||
box-shadow: 0 10px 24px rgba(21, 28, 39, 0.08);
|
||||
background: var(--gia-brand-surface);
|
||||
border: 1px solid var(--gia-brand-border);
|
||||
box-shadow: var(--gia-brand-shadow);
|
||||
color: var(--gia-text);
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
background-color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.gia-brand-logo img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .gia-brand-logo {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-color: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
.section > .container.gia-page-shell,
|
||||
.section > .container {
|
||||
max-width: 1340px;
|
||||
}
|
||||
|
||||
.gia-standard-page-shell {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.gia-page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -110,17 +257,12 @@ a.panel-block:hover {
|
||||
.table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba(248, 250, 252, 0.96) !important;
|
||||
color: #1b1f2a !important;
|
||||
background: var(--gia-surface-3) !important;
|
||||
color: var(--gia-text) !important;
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .table thead th {
|
||||
background: rgba(44, 44, 44, 0.96) !important;
|
||||
color: #f3f5f8 !important;
|
||||
}
|
||||
|
||||
.table td,
|
||||
.table th {
|
||||
vertical-align: top;
|
||||
@@ -131,14 +273,7 @@ a.panel-block:hover {
|
||||
}
|
||||
|
||||
.button.is-light {
|
||||
border-color: rgba(27, 38, 59, 0.12);
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea,
|
||||
.select select {
|
||||
border-color: rgba(27, 38, 59, 0.18);
|
||||
box-shadow: none;
|
||||
border-color: var(--gia-border-strong);
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
@@ -148,6 +283,155 @@ a.panel-block:hover {
|
||||
box-shadow: 0 0 0 0.125em rgba(27, 99, 214, 0.14);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html.gia-has-workspace-root,
|
||||
body.gia-has-workspace {
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.gia-has-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body.gia-has-workspace > .navbar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
body.gia-has-workspace > .section.gia-workspace-page {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.section.gia-workspace-page {
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.gia-workspace-shell {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gia-workspace-main {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gia-workspace-grid-column {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.gia-workspace-grid {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--gia-border);
|
||||
background: var(--gia-surface-1);
|
||||
box-shadow: var(--gia-shadow);
|
||||
}
|
||||
|
||||
.gia-snap-assistant {
|
||||
flex: 0 0 19rem;
|
||||
min-width: 19rem;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--gia-border);
|
||||
background: var(--gia-surface-2);
|
||||
box-shadow: var(--gia-shadow);
|
||||
}
|
||||
|
||||
.gia-snap-assistant.is-hidden,
|
||||
.gia-taskbar.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.gia-snap-assistant-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gia-snap-assistant-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gia-snap-assistant-options {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gia-snap-assistant-options .button {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gia-taskbar {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
border: 1px solid var(--gia-border);
|
||||
border-radius: 1rem;
|
||||
background: var(--gia-surface-1);
|
||||
box-shadow: var(--gia-shadow);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.gia-taskbar ul {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.gia-taskbar li {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.gia-taskbar a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gia-taskbar li.is-active a {
|
||||
background: var(--bulma-link-light);
|
||||
color: var(--bulma-link);
|
||||
}
|
||||
|
||||
.gia-taskbar li.is-minimized a {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
body.gia-has-workspace {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html.gia-has-workspace-root {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-stack-item-content,
|
||||
.floating-window {
|
||||
display: flex !important;
|
||||
@@ -156,17 +440,84 @@ a.panel-block:hover {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.panel {
|
||||
.gia-widget-panel {
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--gia-border);
|
||||
background: var(--gia-surface-2);
|
||||
}
|
||||
|
||||
.gia-widget-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.5rem 0.75rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.gia-widget-heading-main {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.gia-widget-heading-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.gia-widget-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gia-widget-actions {
|
||||
margin: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.gia-widget-actions .button {
|
||||
padding-left: 0.55rem;
|
||||
padding-right: 0.55rem;
|
||||
}
|
||||
|
||||
.gia-widget-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0.75rem;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.gia-widget-control {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.gia-widget-control.gia-widget-control-no-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
min-height: 90%;
|
||||
display: block;
|
||||
.gia-widget-control.gia-widget-control-no-scroll > * {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.grid-stack-item.is-gia-active .gia-widget-panel {
|
||||
border-color: rgba(50, 115, 220, 0.45);
|
||||
box-shadow: 0 0 0 2px rgba(50, 115, 220, 0.16);
|
||||
}
|
||||
|
||||
.floating-window {
|
||||
@@ -178,7 +529,7 @@ a.panel-block:hover {
|
||||
}
|
||||
|
||||
.floating-window .panel {
|
||||
background-color: rgba(250, 250, 250, 0.8) !important;
|
||||
background-color: var(--gia-surface-2) !important;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
@@ -195,10 +546,10 @@ a.panel-block:hover {
|
||||
}
|
||||
|
||||
.osint-table-shell {
|
||||
border: 1px solid rgba(127, 127, 127, 0.2);
|
||||
border: 1px solid var(--gia-border);
|
||||
border-radius: 14px;
|
||||
padding: 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
background: var(--gia-surface-1);
|
||||
}
|
||||
|
||||
.osint-table-toolbar {
|
||||
@@ -210,16 +561,73 @@ a.panel-block:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.gia-badge,
|
||||
.task-ui-badge {
|
||||
border: 1px solid var(--gia-border) !important;
|
||||
color: var(--gia-text) !important;
|
||||
}
|
||||
|
||||
.gia-badge.is-light,
|
||||
.task-ui-badge {
|
||||
background: var(--gia-surface-3) !important;
|
||||
}
|
||||
|
||||
.gia-badge.is-white {
|
||||
background: var(--gia-surface-2) !important;
|
||||
}
|
||||
|
||||
.gia-badge.is-dark {
|
||||
background: var(--gia-text) !important;
|
||||
color: var(--gia-surface-2) !important;
|
||||
}
|
||||
|
||||
.task-ui-badge {
|
||||
background: #f5f5f5 !important;
|
||||
border: 1px solid #dbdbdb !important;
|
||||
color: #1f1f1f !important;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.25em 0.75em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gia-tag-ribbon {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gia-tag-ribbon > .tag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gia-tag-ribbon-main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding-left: 0.7rem;
|
||||
padding-right: 0.7rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tag.is-white {
|
||||
background: var(--gia-surface-2) !important;
|
||||
color: var(--gia-text) !important;
|
||||
border: 1px solid var(--gia-border) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tag.is-dark {
|
||||
background: var(--gia-surface-3) !important;
|
||||
color: var(--gia-text) !important;
|
||||
border: 1px solid var(--gia-border) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tag.is-light:not(.is-primary):not(.is-link):not(.is-info):not(.is-success):not(.is-warning):not(.is-danger):not(.is-dark):not(.is-white):not(.is-black) {
|
||||
background: var(--gia-surface-3) !important;
|
||||
color: var(--gia-text) !important;
|
||||
border: 1px solid var(--gia-border) !important;
|
||||
}
|
||||
|
||||
.osint-results-table th {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
@@ -242,8 +650,8 @@ a.panel-block:hover {
|
||||
}
|
||||
|
||||
.navbar-dropdown .navbar-item.is-current-route {
|
||||
background-color: rgba(50, 115, 220, 0.14) !important;
|
||||
color: #1f4f99 !important;
|
||||
background-color: var(--bulma-link-light) !important;
|
||||
color: var(--bulma-link) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -255,13 +663,21 @@ a.panel-block:hover {
|
||||
|
||||
.brand-theme-toggle {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0.45rem 0.75rem;
|
||||
line-height: 1;
|
||||
width: auto;
|
||||
height: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.brand-theme-toggle:hover,
|
||||
.brand-theme-toggle:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.brand-theme-toggle:focus-visible {
|
||||
outline: 2px solid var(--bulma-link);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.brand-theme-logo {
|
||||
@@ -273,17 +689,120 @@ a.panel-block:hover {
|
||||
}
|
||||
|
||||
.brand-theme-stroke {
|
||||
stroke: #111827;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .brand-theme-stroke {
|
||||
stroke: #f8fafc;
|
||||
stroke: var(--gia-text);
|
||||
}
|
||||
|
||||
.security-page-tabs a {
|
||||
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.gia-settings-nav .tabs ul {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.gia-settings-nav .tabs li {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.gia-settings-nav .tabs a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gia-send-composer {
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--bulma-border, #dbdbdb);
|
||||
border-radius: 0.875rem;
|
||||
background: var(--bulma-scheme-main-bis, #f7f8fa);
|
||||
}
|
||||
|
||||
.gia-send-composer-row {
|
||||
align-items: stretch;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gia-send-composer-input-wrap {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gia-send-composer-input {
|
||||
min-height: 2.75rem;
|
||||
max-height: 8rem;
|
||||
resize: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.gia-send-composer-action {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gia-send-composer-button {
|
||||
height: 100%;
|
||||
min-height: 2.75rem;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.navbar-dropdown.gia-navbar-dropdown {
|
||||
max-width: min(24rem, calc(100vw - 1rem));
|
||||
max-height: min(80vh, 34rem) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
@media print, screen and (min-width: 1024px) {
|
||||
.navbar-end .has-dropdown > .navbar-dropdown.gia-navbar-dropdown {
|
||||
left: auto !important;
|
||||
right: 0 !important;
|
||||
inset-inline-start: auto !important;
|
||||
inset-inline-end: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section.gia-workspace-page {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.gia-workspace-main {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gia-snap-assistant {
|
||||
min-width: 0;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.gia-widget-heading {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.gia-widget-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.gia-send-composer-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gia-send-composer-input {
|
||||
border-radius: var(--bulma-radius, 0.375rem);
|
||||
}
|
||||
|
||||
.gia-send-composer-action {
|
||||
display: block;
|
||||
margin-top: 0.75rem;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.gia-send-composer-button {
|
||||
width: 100%;
|
||||
border-radius: var(--bulma-radius, 0.375rem);
|
||||
}
|
||||
}
|
||||
|
||||
.reduced-motion,
|
||||
.reduced-motion * {
|
||||
animation-duration: 0.01ms !important;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
|
||||
{
|
||||
const data = document.currentScript.dataset;
|
||||
const isDebug = data.debug === "True";
|
||||
|
||||
if (isDebug) {
|
||||
document.addEventListener("htmx:beforeOnLoad", function (event) {
|
||||
const xhr = event.detail.xhr;
|
||||
if (xhr.status == 500 || xhr.status == 404) {
|
||||
// Tell htmx to stop processing this response
|
||||
event.stopPropagation();
|
||||
|
||||
document.children[0].innerHTML = xhr.response;
|
||||
|
||||
// Run Django’s inline script
|
||||
// (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript
|
||||
(1, eval)(document.scripts[0].innerText);
|
||||
// Need to directly call Django’s onload function since browser won’t
|
||||
window.onload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
1
core/static/js/bulma-calendar.min.js
vendored
1
core/static/js/bulma-calendar.min.js
vendored
File diff suppressed because one or more lines are too long
1
core/static/js/bulma-slider.min.js
vendored
1
core/static/js/bulma-slider.min.js
vendored
File diff suppressed because one or more lines are too long
1
core/static/js/bulma-tagsinput.min.js
vendored
1
core/static/js/bulma-tagsinput.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,259 +0,0 @@
|
||||
// Author: Grzegorz Tężycki
|
||||
|
||||
$(document).ready(function(){
|
||||
|
||||
// In web storage is saved structure like that:
|
||||
// localStorage['django_tables2_column_shifter'] = {
|
||||
// 'table_class_container1' : {
|
||||
// 'id' : 'on',
|
||||
// 'col1' : 'off',
|
||||
// 'col2' : 'on',
|
||||
// 'col3' : 'on',
|
||||
// },
|
||||
// 'table_class_container2' : {
|
||||
// 'id' : 'on',
|
||||
// 'col1' : 'on'
|
||||
// },
|
||||
// }
|
||||
|
||||
// main name for key in web storage
|
||||
var COLUMN_SHIFTER_STORAGE_ACCESOR = "django_tables2_column_shifter";
|
||||
|
||||
// Return storage structure for shifter
|
||||
// If structure does'n exist in web storage
|
||||
// will be return empty object
|
||||
var get_column_shifter_storage = function(){
|
||||
var storage = localStorage.getItem(COLUMN_SHIFTER_STORAGE_ACCESOR);
|
||||
if (storage === null) {
|
||||
storage = {
|
||||
"drilldown-table": {
|
||||
"date": "off",
|
||||
"time": "off",
|
||||
"id": "off",
|
||||
"host": "off",
|
||||
"ident": "off",
|
||||
"channel": "off",
|
||||
"net": "off",
|
||||
"num": "off",
|
||||
"channel_nsfw": "off",
|
||||
"channel_category": "off",
|
||||
"channel_category_id": "off",
|
||||
"channel_category_nsfw": "off",
|
||||
"channel_id": "off",
|
||||
"guild_member_count": "off",
|
||||
"bot": "off",
|
||||
"msg_id": "off",
|
||||
"user": "off",
|
||||
"net_id": "off",
|
||||
"user_id": "off",
|
||||
"nick_id": "off",
|
||||
"status": "off",
|
||||
"num_users": "off",
|
||||
"num_chans": "off",
|
||||
"exemption": "off",
|
||||
// "version_sentiment": "off",
|
||||
"sentiment": "off",
|
||||
"num": "off",
|
||||
"online": "off",
|
||||
"mtype": "off",
|
||||
"realname": "off",
|
||||
"server": "off",
|
||||
"mtype": "off",
|
||||
"hidden": "off",
|
||||
"filename": "off",
|
||||
"file_md5": "off",
|
||||
"file_ext": "off",
|
||||
"file_size": "off",
|
||||
"lang_code": "off",
|
||||
"tokens": "off",
|
||||
"rule_id": "off",
|
||||
"index": "off",
|
||||
"meta": "off",
|
||||
"match_ts": "off",
|
||||
"batch_id": "off"
|
||||
//"lang_name": "off",
|
||||
// "words_noun": "off",
|
||||
// "words_adj": "off",
|
||||
// "words_verb": "off",
|
||||
// "words_adv": "off"
|
||||
},
|
||||
};
|
||||
} else {
|
||||
storage = JSON.parse(storage);
|
||||
}
|
||||
return storage;
|
||||
};
|
||||
|
||||
// Save structure in web storage
|
||||
var set_column_shifter_storage = function(storage){
|
||||
var json_storage = JSON.stringify(storage)
|
||||
localStorage.setItem(COLUMN_SHIFTER_STORAGE_ACCESOR, json_storage);
|
||||
};
|
||||
|
||||
// Remember state for single button
|
||||
var save_btn_state = function($btn){
|
||||
|
||||
// Take css class for container with table
|
||||
var table_class_container = $btn.data("table-class-container");
|
||||
// Take html object with table
|
||||
var $table_class_container = $("#" + table_class_container);
|
||||
// Take single button statne ("on" / "off")
|
||||
var state = $btn.data("state");
|
||||
// td-class is a real column name in table
|
||||
var td_class = $btn.data("td-class");
|
||||
var storage = get_column_shifter_storage();
|
||||
// Table id
|
||||
var id = $table_class_container.attr("id");
|
||||
|
||||
// Checking if the ID is already in storage
|
||||
if (id in storage) {
|
||||
data = storage[id]
|
||||
} else {
|
||||
data = {}
|
||||
storage[id] = data;
|
||||
}
|
||||
|
||||
// Save state for table column in storage
|
||||
data[td_class] = state;
|
||||
set_column_shifter_storage(storage);
|
||||
};
|
||||
|
||||
// Load states for buttons from storage for single tabel
|
||||
var load_states = function($table_class_container) {
|
||||
var storage = get_column_shifter_storage();
|
||||
// Table id
|
||||
var id = $table_class_container.attr("id");
|
||||
var data = {};
|
||||
|
||||
// Checking if the ID is already in storage
|
||||
if (id in storage) {
|
||||
data = storage[id]
|
||||
|
||||
// For each shifter button set state
|
||||
$table_class_container.find(".btn-shift-column").each(function(){
|
||||
var $btn = $(this);
|
||||
var td_class = $btn.data("td-class");
|
||||
|
||||
// If name of column is in store then get state
|
||||
// and set state
|
||||
if (td_class in data) {
|
||||
var state = data[td_class]
|
||||
set_btn_state($btn, state);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Show table content and hide spiner
|
||||
var show_table_content = function($table_class_container){
|
||||
$table_class_container.find("#loader").hide();
|
||||
$table_class_container.find("#table-container").show();
|
||||
};
|
||||
|
||||
// Load buttons states for all button in page
|
||||
var load_state_for_all_containters = function(){
|
||||
$(".column-shifter-container").each(function(){
|
||||
$table_class_container = $(this);
|
||||
|
||||
// Load states for all buttons in single container
|
||||
load_states($table_class_container);
|
||||
|
||||
// When states was loaded then table must be show and
|
||||
// loader (spiner) must be hide
|
||||
show_table_content($table_class_container);
|
||||
});
|
||||
};
|
||||
|
||||
// change visibility column for single button
|
||||
// if button has state "on" then show column
|
||||
// else then column will be hide
|
||||
shift_column = function( $btn ){
|
||||
// button state
|
||||
var state = $btn.data("state");
|
||||
|
||||
// td-class is a real column name in table
|
||||
var td_class = $btn.data("td-class");
|
||||
var table_class_container = $btn.data("table-class-container");
|
||||
var $table_class_container = $("#" + table_class_container);
|
||||
var $table = $table_class_container.find("table");
|
||||
var $cels = $table.find("." + td_class);
|
||||
|
||||
if ( state === "on" ) {
|
||||
$cels.show();
|
||||
} else {
|
||||
$cels.hide();
|
||||
}
|
||||
};
|
||||
|
||||
// Shift visibility for all columns
|
||||
shift_columns = function(){
|
||||
var cols = $(".btn-shift-column");
|
||||
var i, len = cols.length;
|
||||
for (i=0; i < len; i++) {
|
||||
shift_column($(cols[i]));
|
||||
}
|
||||
};
|
||||
|
||||
// Set icon imgae visibility for button state
|
||||
var set_icon_for_state = function( $btn, state ) {
|
||||
if (state === "on") {
|
||||
$btn.find("span.uncheck").hide();
|
||||
$btn.find("span.check").show();
|
||||
} else {
|
||||
$btn.find("span.check").hide();
|
||||
$btn.find("span.uncheck").show();
|
||||
}
|
||||
};
|
||||
|
||||
// Set state for single button
|
||||
var set_btn_state = function($btn, state){
|
||||
$btn.data('state', state);
|
||||
set_icon_for_state($btn, state);
|
||||
}
|
||||
|
||||
// Change state for single button
|
||||
var change_btn_state = function($btn){
|
||||
var state = $btn.data("state");
|
||||
|
||||
if (state === "on") {
|
||||
state = "off"
|
||||
} else {
|
||||
state = "on"
|
||||
}
|
||||
set_btn_state($btn, state);
|
||||
};
|
||||
|
||||
// Run show/hide when click on button
|
||||
$(".btn-shift-column").on("click", function(event){
|
||||
var $btn = $(this);
|
||||
event.stopPropagation();
|
||||
change_btn_state($btn);
|
||||
shift_column($btn);
|
||||
save_btn_state($btn);
|
||||
});
|
||||
|
||||
// Load saved states for all tables
|
||||
load_state_for_all_containters();
|
||||
|
||||
// show or hide columns based on data from web storage
|
||||
shift_columns();
|
||||
|
||||
// Add API method for retrieving non-visible cols for table
|
||||
// Pass the 0-based index of the table or leave the parameter
|
||||
// empty to return the hidden cols for the 1st table found
|
||||
$.django_tables2_column_shifter_hidden = function(idx) {
|
||||
if(idx==undefined) {
|
||||
idx = 0;
|
||||
}
|
||||
return $('#table-container').eq(idx).find('.btn-shift-column').filter(function(z) {
|
||||
return $(this).data('state')=='off'
|
||||
}).map(function(z) {
|
||||
return $(this).data('td-class')
|
||||
}).toArray();
|
||||
}
|
||||
const event = new Event('restore-scroll');
|
||||
document.dispatchEvent(event);
|
||||
const event2 = new Event('load-widget-results');
|
||||
document.dispatchEvent(event2);
|
||||
|
||||
});
|
||||
139
core/static/js/compose-panel-core.js
Normal file
139
core/static/js/compose-panel-core.js
Normal file
@@ -0,0 +1,139 @@
|
||||
(function () {
|
||||
if (window.GIAComposePanelCore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PANEL_SELECTOR = ".compose-shell[data-compose-panel='1']";
|
||||
window.giaComposePanels = window.giaComposePanels || {};
|
||||
|
||||
const collectPanels = function (root) {
|
||||
const panels = [];
|
||||
if (!root) {
|
||||
return panels;
|
||||
}
|
||||
if (root.matches && root.matches(PANEL_SELECTOR)) {
|
||||
panels.push(root);
|
||||
}
|
||||
if (root.querySelectorAll) {
|
||||
root.querySelectorAll(PANEL_SELECTOR).forEach(function (panel) {
|
||||
panels.push(panel);
|
||||
});
|
||||
}
|
||||
return panels;
|
||||
};
|
||||
|
||||
const toInt = function (value) {
|
||||
const parsed = parseInt(value || "0", 10);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
};
|
||||
|
||||
const parseJsonSafe = function (value, fallback) {
|
||||
try {
|
||||
return JSON.parse(String(value || ""));
|
||||
} catch (_err) {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
const createNode = function (tagName, className, text) {
|
||||
const node = document.createElement(tagName);
|
||||
if (className) {
|
||||
node.className = className;
|
||||
}
|
||||
if (text !== undefined && text !== null) {
|
||||
node.textContent = String(text);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
const normalizeSnippet = function (value) {
|
||||
const compact = String(value || "").replace(/\s+/g, " ").trim();
|
||||
if (!compact) {
|
||||
return "(no text)";
|
||||
}
|
||||
if (compact.length <= 120) {
|
||||
return compact;
|
||||
}
|
||||
return compact.slice(0, 117).trimEnd() + "...";
|
||||
};
|
||||
|
||||
const titleCase = function (value) {
|
||||
const raw = String(value || "").trim().toLowerCase();
|
||||
if (!raw) {
|
||||
return "";
|
||||
}
|
||||
if (raw === "whatsapp") {
|
||||
return "WhatsApp";
|
||||
}
|
||||
if (raw === "xmpp") {
|
||||
return "XMPP";
|
||||
}
|
||||
return raw.charAt(0).toUpperCase() + raw.slice(1);
|
||||
};
|
||||
|
||||
const normalizeIdentifierForService = function (service, identifier) {
|
||||
const serviceKey = String(service || "").trim().toLowerCase();
|
||||
const raw = String(identifier || "").trim();
|
||||
if (serviceKey === "whatsapp" && raw.includes("@")) {
|
||||
return raw.split("@", 1)[0].trim();
|
||||
}
|
||||
return raw;
|
||||
};
|
||||
|
||||
const buildComposeUrl = function (renderMode, service, identifier, personId) {
|
||||
const serviceKey = String(service || "").trim().toLowerCase();
|
||||
const identifierValue = normalizeIdentifierForService(serviceKey, identifier);
|
||||
if (!serviceKey || !identifierValue) {
|
||||
return "";
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set("service", serviceKey);
|
||||
params.set("identifier", identifierValue);
|
||||
if (personId) {
|
||||
params.set("person", String(personId || "").trim());
|
||||
}
|
||||
return (renderMode === "page" ? "/compose/page/" : "/compose/widget/")
|
||||
+ "?"
|
||||
+ params.toString();
|
||||
};
|
||||
|
||||
const parseServiceMap = function (optionNode) {
|
||||
const fallbackService = String(
|
||||
(optionNode && optionNode.dataset && optionNode.dataset.service) || ""
|
||||
).trim().toLowerCase();
|
||||
const fallbackIdentifier = String((optionNode && optionNode.value) || "").trim();
|
||||
const fallback = {};
|
||||
if (fallbackService && fallbackIdentifier) {
|
||||
fallback[fallbackService] = fallbackIdentifier;
|
||||
}
|
||||
if (!optionNode || !optionNode.dataset) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = parseJsonSafe(optionNode.dataset.serviceMap || "{}", fallback);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return fallback;
|
||||
}
|
||||
const normalized = {};
|
||||
Object.keys(parsed).forEach(function (key) {
|
||||
const serviceKey = String(key || "").trim().toLowerCase();
|
||||
const identifierValue = String(parsed[key] || "").trim();
|
||||
if (serviceKey && identifierValue) {
|
||||
normalized[serviceKey] = identifierValue;
|
||||
}
|
||||
});
|
||||
return Object.keys(normalized).length ? normalized : fallback;
|
||||
};
|
||||
|
||||
window.GIAComposePanelCore = {
|
||||
PANEL_SELECTOR: PANEL_SELECTOR,
|
||||
buildComposeUrl: buildComposeUrl,
|
||||
collectPanels: collectPanels,
|
||||
createNode: createNode,
|
||||
normalizeIdentifierForService: normalizeIdentifierForService,
|
||||
normalizeSnippet: normalizeSnippet,
|
||||
parseJsonSafe: parseJsonSafe,
|
||||
parseServiceMap: parseServiceMap,
|
||||
titleCase: titleCase,
|
||||
toInt: toInt,
|
||||
};
|
||||
})();
|
||||
321
core/static/js/compose-panel-send.js
Normal file
321
core/static/js/compose-panel-send.js
Normal file
@@ -0,0 +1,321 @@
|
||||
(function () {
|
||||
if (window.GIAComposePanelSend) {
|
||||
return;
|
||||
}
|
||||
|
||||
const core = window.GIAComposePanelCore;
|
||||
if (!core) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createController = function (config) {
|
||||
const panel = config.panel;
|
||||
const panelId = config.panelId;
|
||||
const state = config.state;
|
||||
const thread = config.thread;
|
||||
const form = config.form;
|
||||
const textarea = config.textarea;
|
||||
const statusBox = config.statusBox;
|
||||
const manualConfirm = config.manualConfirm;
|
||||
const armInput = config.armInput;
|
||||
const confirmInput = config.confirmInput;
|
||||
const sendButton = config.sendButton;
|
||||
const sendCapable = config.sendCapable;
|
||||
const csrfToken = config.csrfToken;
|
||||
const queryParams = config.queryParams;
|
||||
const poll = config.poll;
|
||||
const clearReplyTarget = config.clearReplyTarget;
|
||||
const autosize = config.autosize;
|
||||
const flashCompose = config.flashCompose;
|
||||
const setStatus = config.setStatus;
|
||||
|
||||
let transientCancelButton = null;
|
||||
let persistentCancelWrap = null;
|
||||
|
||||
const postFormJson = async function (url, params) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": csrfToken,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Request failed");
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const cancelSendRequest = function (commandId) {
|
||||
return postFormJson(
|
||||
String(panel.dataset.cancelSendUrl || ""),
|
||||
queryParams({ command_id: String(commandId || "") })
|
||||
);
|
||||
};
|
||||
|
||||
const hideTransientCancelButton = function () {
|
||||
if (!transientCancelButton) {
|
||||
return;
|
||||
}
|
||||
transientCancelButton.remove();
|
||||
transientCancelButton = null;
|
||||
};
|
||||
|
||||
const showTransientCancelButton = function () {
|
||||
if (!statusBox || transientCancelButton) {
|
||||
return;
|
||||
}
|
||||
transientCancelButton = core.createNode(
|
||||
"button",
|
||||
"button is-danger is-light is-small compose-cancel-send-btn",
|
||||
"Cancel Send"
|
||||
);
|
||||
transientCancelButton.type = "button";
|
||||
transientCancelButton.addEventListener("click", async function () {
|
||||
try {
|
||||
await cancelSendRequest("");
|
||||
} catch (_err) {
|
||||
// Ignore cancel failures.
|
||||
} finally {
|
||||
hideTransientCancelButton();
|
||||
}
|
||||
});
|
||||
statusBox.appendChild(transientCancelButton);
|
||||
};
|
||||
|
||||
const hidePersistentCancelButton = function () {
|
||||
if (!persistentCancelWrap) {
|
||||
return;
|
||||
}
|
||||
persistentCancelWrap.remove();
|
||||
persistentCancelWrap = null;
|
||||
};
|
||||
|
||||
const stopPendingCommandPolling = function () {
|
||||
if (state.pendingCommandPoll) {
|
||||
clearInterval(state.pendingCommandPoll);
|
||||
state.pendingCommandPoll = null;
|
||||
}
|
||||
state.pendingCommandId = "";
|
||||
state.pendingCommandAttempts = 0;
|
||||
state.pendingCommandStartedAt = 0;
|
||||
state.pendingCommandInFlight = false;
|
||||
};
|
||||
|
||||
const showPersistentCancelButton = function (commandId) {
|
||||
hidePersistentCancelButton();
|
||||
if (!statusBox) {
|
||||
return;
|
||||
}
|
||||
persistentCancelWrap = core.createNode("div", "compose-persistent-cancel");
|
||||
const button = core.createNode(
|
||||
"button",
|
||||
"button is-danger is-light is-small compose-persistent-cancel-btn",
|
||||
"Cancel Queued Send"
|
||||
);
|
||||
button.type = "button";
|
||||
button.addEventListener("click", async function () {
|
||||
try {
|
||||
await cancelSendRequest(commandId);
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
setStatus("Send cancelled.", "warning");
|
||||
await poll(true);
|
||||
} catch (_err) {
|
||||
hidePersistentCancelButton();
|
||||
}
|
||||
});
|
||||
persistentCancelWrap.appendChild(button);
|
||||
statusBox.appendChild(persistentCancelWrap);
|
||||
};
|
||||
|
||||
const pollPendingCommandResult = async function (commandId) {
|
||||
const url = new URL(
|
||||
String(panel.dataset.commandResultUrl || ""),
|
||||
window.location.origin
|
||||
);
|
||||
url.searchParams.set("service", thread.dataset.service || "");
|
||||
url.searchParams.set("command_id", commandId);
|
||||
url.searchParams.set("format", "json");
|
||||
const response = await fetch(url.toString(), {
|
||||
credentials: "same-origin",
|
||||
headers: { "HX-Request": "true" },
|
||||
});
|
||||
if (!response.ok || response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const startPendingCommandPolling = function (commandId) {
|
||||
if (!commandId) {
|
||||
return;
|
||||
}
|
||||
stopPendingCommandPolling();
|
||||
state.pendingCommandId = commandId;
|
||||
state.pendingCommandStartedAt = Date.now();
|
||||
showPersistentCancelButton(commandId);
|
||||
state.pendingCommandPoll = setInterval(async function () {
|
||||
if (state.pendingCommandInFlight) {
|
||||
return;
|
||||
}
|
||||
state.pendingCommandAttempts += 1;
|
||||
if (
|
||||
state.pendingCommandAttempts > 14
|
||||
|| (Date.now() - state.pendingCommandStartedAt) > 45000
|
||||
) {
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
setStatus(
|
||||
"Send timed out waiting for runtime result. Please retry.",
|
||||
"warning"
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
state.pendingCommandInFlight = true;
|
||||
const payload = await pollPendingCommandResult(commandId);
|
||||
if (!payload || payload.pending !== false) {
|
||||
return;
|
||||
}
|
||||
const result = payload.result || {};
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
if (result.ok) {
|
||||
setStatus("", "success");
|
||||
textarea.value = "";
|
||||
clearReplyTarget();
|
||||
autosize();
|
||||
flashCompose("is-send-success");
|
||||
await poll(true);
|
||||
return;
|
||||
}
|
||||
setStatus(String(result.error || "Send failed."), "danger");
|
||||
flashCompose("is-send-fail");
|
||||
await poll(true);
|
||||
} catch (_err) {
|
||||
// Ignore transient failures; the next poll can recover.
|
||||
} finally {
|
||||
state.pendingCommandInFlight = false;
|
||||
}
|
||||
}, 3500);
|
||||
};
|
||||
|
||||
const updateManualSafety = function () {
|
||||
const confirmed = !!(manualConfirm && manualConfirm.checked);
|
||||
if (armInput) {
|
||||
armInput.value = confirmed ? "1" : "0";
|
||||
}
|
||||
if (confirmInput) {
|
||||
confirmInput.value = confirmed ? "1" : "0";
|
||||
}
|
||||
if (sendButton) {
|
||||
sendButton.disabled = !sendCapable || !confirmed;
|
||||
}
|
||||
};
|
||||
|
||||
const bindSendEvents = function () {
|
||||
textarea.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "Enter" || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (sendButton && sendButton.disabled) {
|
||||
setStatus("Enable send confirmation before sending.", "warning");
|
||||
return;
|
||||
}
|
||||
form.requestSubmit();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", function () {
|
||||
if (sendButton && sendButton.disabled) {
|
||||
return;
|
||||
}
|
||||
showTransientCancelButton();
|
||||
});
|
||||
|
||||
form.addEventListener("htmx:afterRequest", function () {
|
||||
hideTransientCancelButton();
|
||||
textarea.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const bindDocumentEvents = function () {
|
||||
state.eventHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const sourcePanelId = String(detail.panel_id || "");
|
||||
if (sourcePanelId && sourcePanelId !== panelId) {
|
||||
return;
|
||||
}
|
||||
poll(true);
|
||||
};
|
||||
document.body.addEventListener("composeMessageSent", state.eventHandler);
|
||||
|
||||
state.sendResultHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const sourcePanelId = String(detail.panel_id || "");
|
||||
if (sourcePanelId && sourcePanelId !== panelId) {
|
||||
return;
|
||||
}
|
||||
hideTransientCancelButton();
|
||||
if (detail.ok) {
|
||||
flashCompose("is-send-success");
|
||||
textarea.value = "";
|
||||
clearReplyTarget();
|
||||
autosize();
|
||||
poll(true);
|
||||
} else {
|
||||
flashCompose("is-send-fail");
|
||||
if (detail.message) {
|
||||
setStatus(detail.message, detail.level || "danger");
|
||||
}
|
||||
}
|
||||
textarea.focus();
|
||||
};
|
||||
document.body.addEventListener("composeSendResult", state.sendResultHandler);
|
||||
|
||||
state.commandIdHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const commandId = String(
|
||||
detail.command_id
|
||||
|| (detail.composeSendCommandId && detail.composeSendCommandId.command_id)
|
||||
|| ""
|
||||
).trim();
|
||||
if (commandId) {
|
||||
startPendingCommandPolling(commandId);
|
||||
}
|
||||
};
|
||||
document.body.addEventListener(
|
||||
"composeSendCommandId",
|
||||
state.commandIdHandler
|
||||
);
|
||||
};
|
||||
|
||||
const init = function () {
|
||||
bindSendEvents();
|
||||
bindDocumentEvents();
|
||||
if (manualConfirm) {
|
||||
manualConfirm.addEventListener("change", updateManualSafety);
|
||||
manualConfirm.dispatchEvent(new Event("change"));
|
||||
} else {
|
||||
updateManualSafety();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
resetForContextSwitch: function () {
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
hideTransientCancelButton();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
window.GIAComposePanelSend = {
|
||||
createController: createController,
|
||||
};
|
||||
})();
|
||||
504
core/static/js/compose-panel-thread.js
Normal file
504
core/static/js/compose-panel-thread.js
Normal file
@@ -0,0 +1,504 @@
|
||||
(function () {
|
||||
if (window.GIAComposePanelThread) {
|
||||
return;
|
||||
}
|
||||
|
||||
const core = window.GIAComposePanelCore;
|
||||
if (!core) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createController = function (config) {
|
||||
const panel = config.panel;
|
||||
const state = config.state;
|
||||
const thread = config.thread;
|
||||
const textarea = config.textarea;
|
||||
const typingNode = config.typingNode;
|
||||
const hiddenReplyTo = config.hiddenReplyTo;
|
||||
const replyBanner = config.replyBanner;
|
||||
const replyBannerText = config.replyBannerText;
|
||||
const replyClearBtn = config.replyClearBtn;
|
||||
const platformSelect = config.platformSelect;
|
||||
const contactSelect = config.contactSelect;
|
||||
const hiddenService = config.hiddenService;
|
||||
const hiddenIdentifier = config.hiddenIdentifier;
|
||||
const hiddenPerson = config.hiddenPerson;
|
||||
const metaLine = config.metaLine;
|
||||
const renderMode = config.renderMode;
|
||||
|
||||
let lastTs = core.toInt(thread.dataset.lastTs);
|
||||
let beforeContextReset = null;
|
||||
|
||||
const nearBottom = function () {
|
||||
return thread.scrollHeight - thread.scrollTop - thread.clientHeight < 120;
|
||||
};
|
||||
|
||||
const scrollToBottom = function (force) {
|
||||
if (force || nearBottom()) {
|
||||
thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const queryParams = function (extraParams) {
|
||||
const params = new URLSearchParams();
|
||||
params.set("service", thread.dataset.service || "");
|
||||
params.set("identifier", thread.dataset.identifier || "");
|
||||
if (thread.dataset.person) {
|
||||
params.set("person", thread.dataset.person);
|
||||
}
|
||||
params.set("limit", thread.dataset.limit || "60");
|
||||
const extras =
|
||||
extraParams && typeof extraParams === "object" ? extraParams : {};
|
||||
Object.keys(extras).forEach(function (key) {
|
||||
const value = extras[key];
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return;
|
||||
}
|
||||
params.set(String(key), String(value));
|
||||
});
|
||||
return params;
|
||||
};
|
||||
|
||||
const ensureEmptyState = function (messageText) {
|
||||
if (thread.querySelector(".compose-row")) {
|
||||
const empty = thread.querySelector(".compose-empty");
|
||||
if (empty) {
|
||||
empty.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
let empty = thread.querySelector(".compose-empty");
|
||||
if (!empty) {
|
||||
empty = core.createNode("p", "compose-empty");
|
||||
thread.appendChild(empty);
|
||||
}
|
||||
empty.textContent = String(
|
||||
messageText || "No stored messages for this contact yet."
|
||||
);
|
||||
};
|
||||
|
||||
const rowByMessageId = function (messageId) {
|
||||
const targetId = String(messageId || "").trim();
|
||||
if (!targetId) {
|
||||
return null;
|
||||
}
|
||||
return thread.querySelector(
|
||||
'.compose-row[data-message-id="' + targetId + '"]'
|
||||
);
|
||||
};
|
||||
|
||||
const clearReplySelectionClass = function () {
|
||||
thread
|
||||
.querySelectorAll(".compose-row.compose-reply-selected")
|
||||
.forEach(function (row) {
|
||||
row.classList.remove("compose-reply-selected");
|
||||
});
|
||||
};
|
||||
|
||||
const flashReplyTarget = function (row) {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.classList.remove("is-target-flash");
|
||||
void row.offsetWidth;
|
||||
row.classList.add("is-target-flash");
|
||||
window.setTimeout(function () {
|
||||
row.classList.remove("is-target-flash");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const clearReplyTarget = function () {
|
||||
if (hiddenReplyTo) {
|
||||
hiddenReplyTo.value = "";
|
||||
}
|
||||
if (replyBanner) {
|
||||
replyBanner.classList.add("is-hidden");
|
||||
}
|
||||
if (replyBannerText) {
|
||||
replyBannerText.textContent = "";
|
||||
}
|
||||
clearReplySelectionClass();
|
||||
};
|
||||
|
||||
const setReplyTarget = function (messageId, preview) {
|
||||
const targetId = String(messageId || "").trim();
|
||||
if (!targetId) {
|
||||
clearReplyTarget();
|
||||
return;
|
||||
}
|
||||
const row = rowByMessageId(targetId);
|
||||
const snippet = core.normalizeSnippet(
|
||||
(row && row.dataset ? row.dataset.replySnippet : "") || preview || ""
|
||||
);
|
||||
if (hiddenReplyTo) {
|
||||
hiddenReplyTo.value = targetId;
|
||||
}
|
||||
if (replyBannerText) {
|
||||
replyBannerText.textContent = snippet;
|
||||
}
|
||||
if (replyBanner) {
|
||||
replyBanner.classList.remove("is-hidden");
|
||||
}
|
||||
clearReplySelectionClass();
|
||||
if (row) {
|
||||
row.classList.add("compose-reply-selected");
|
||||
}
|
||||
};
|
||||
|
||||
const parseMessageRows = function (html) {
|
||||
const markup = String(html || "").trim();
|
||||
if (!markup) {
|
||||
return [];
|
||||
}
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = markup;
|
||||
return Array.from(template.content.querySelectorAll(".compose-row"));
|
||||
};
|
||||
|
||||
const insertRowByTs = function (row) {
|
||||
const newTs = core.toInt(row.dataset.ts);
|
||||
const rows = Array.from(thread.querySelectorAll(".compose-row"));
|
||||
if (!rows.length) {
|
||||
thread.appendChild(row);
|
||||
return;
|
||||
}
|
||||
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
||||
const existing = rows[index];
|
||||
if (core.toInt(existing.dataset.ts) <= newTs) {
|
||||
if (existing.nextSibling) {
|
||||
thread.insertBefore(row, existing.nextSibling);
|
||||
} else {
|
||||
thread.appendChild(row);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
thread.insertBefore(row, rows[0]);
|
||||
};
|
||||
|
||||
const upsertMessageRow = function (row) {
|
||||
if (!row || !row.classList || !row.classList.contains("compose-row")) {
|
||||
return;
|
||||
}
|
||||
const messageId = String((row.dataset && row.dataset.messageId) || "").trim();
|
||||
if (messageId) {
|
||||
const existing = rowByMessageId(messageId);
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
}
|
||||
const empty = thread.querySelector(".compose-empty");
|
||||
if (empty) {
|
||||
empty.remove();
|
||||
}
|
||||
insertRowByTs(row);
|
||||
lastTs = Math.max(lastTs, core.toInt(row.dataset.ts));
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
};
|
||||
|
||||
const appendMessageHtml = function (html, forceScroll) {
|
||||
const rows = parseMessageRows(html);
|
||||
const shouldStick = forceScroll || nearBottom();
|
||||
rows.forEach(function (msg) {
|
||||
upsertMessageRow(msg);
|
||||
});
|
||||
if (rows.length) {
|
||||
scrollToBottom(shouldStick);
|
||||
}
|
||||
ensureEmptyState();
|
||||
};
|
||||
|
||||
const applyTyping = function (payload) {
|
||||
if (!typingNode) {
|
||||
return;
|
||||
}
|
||||
const typingPayload =
|
||||
payload && typeof payload === "object" ? payload : {};
|
||||
if (!typingPayload.typing) {
|
||||
typingNode.classList.add("is-hidden");
|
||||
return;
|
||||
}
|
||||
const displayName = String(typingPayload.display_name || "").trim();
|
||||
typingNode.textContent = (displayName || "Contact") + " is typing...";
|
||||
typingNode.classList.remove("is-hidden");
|
||||
};
|
||||
|
||||
const closeSocket = function () {
|
||||
if (!state.socket) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
state.socket.close();
|
||||
} catch (_err) {
|
||||
// Ignore close failures.
|
||||
}
|
||||
state.socket = null;
|
||||
state.websocketReady = false;
|
||||
};
|
||||
|
||||
const poll = async function (forceScroll) {
|
||||
if (state.polling || state.websocketReady) {
|
||||
return;
|
||||
}
|
||||
state.polling = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
thread.dataset.pollUrl + "?" + queryParams({ after_ts: String(lastTs) }),
|
||||
{
|
||||
method: "GET",
|
||||
credentials: "same-origin",
|
||||
headers: { Accept: "application/json" },
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const payload = await response.json();
|
||||
appendMessageHtml(payload.messages_html || "", forceScroll);
|
||||
applyTyping(payload.typing);
|
||||
if (payload.last_ts !== undefined && payload.last_ts !== null) {
|
||||
lastTs = Math.max(lastTs, core.toInt(payload.last_ts));
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug("compose poll error", err);
|
||||
} finally {
|
||||
state.polling = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setupWebSocket = function () {
|
||||
const wsPath = String(thread.dataset.wsUrl || "").trim();
|
||||
if (!wsPath || !window.WebSocket) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||
const socket = new WebSocket(protocol + window.location.host + wsPath);
|
||||
state.socket = socket;
|
||||
socket.onopen = function () {
|
||||
state.websocketReady = true;
|
||||
try {
|
||||
socket.send(JSON.stringify({ kind: "sync", last_ts: lastTs }));
|
||||
} catch (_err) {
|
||||
// Ignore sync send errors.
|
||||
}
|
||||
};
|
||||
socket.onmessage = function (event) {
|
||||
const payload = core.parseJsonSafe(event.data || "{}", {});
|
||||
appendMessageHtml(payload.messages_html || "", false);
|
||||
applyTyping(payload.typing);
|
||||
if (payload.last_ts !== undefined && payload.last_ts !== null) {
|
||||
lastTs = Math.max(lastTs, core.toInt(payload.last_ts));
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
}
|
||||
};
|
||||
socket.onclose = function () {
|
||||
state.websocketReady = false;
|
||||
if (state.socket === socket) {
|
||||
state.socket = null;
|
||||
}
|
||||
};
|
||||
socket.onerror = function () {
|
||||
state.websocketReady = false;
|
||||
};
|
||||
} catch (_err) {
|
||||
state.websocketReady = false;
|
||||
state.socket = null;
|
||||
}
|
||||
};
|
||||
|
||||
const switchThreadContext = function (
|
||||
nextService,
|
||||
nextIdentifier,
|
||||
nextPersonId,
|
||||
nextUrl
|
||||
) {
|
||||
const service = String(nextService || "").trim().toLowerCase();
|
||||
const identifier = core.normalizeIdentifierForService(
|
||||
service,
|
||||
nextIdentifier
|
||||
);
|
||||
const personId = String(nextPersonId || "").trim();
|
||||
if (!service || !identifier) {
|
||||
return;
|
||||
}
|
||||
if (renderMode === "page" && nextUrl) {
|
||||
window.location.assign(String(nextUrl));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
String(thread.dataset.service || "").toLowerCase() === service
|
||||
&& String(thread.dataset.identifier || "") === identifier
|
||||
&& String(thread.dataset.person || "") === personId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (typeof beforeContextReset === "function") {
|
||||
beforeContextReset();
|
||||
}
|
||||
thread.dataset.service = service;
|
||||
thread.dataset.identifier = identifier;
|
||||
if (personId) {
|
||||
thread.dataset.person = personId;
|
||||
} else {
|
||||
delete thread.dataset.person;
|
||||
}
|
||||
if (hiddenService) {
|
||||
hiddenService.value = service;
|
||||
}
|
||||
if (hiddenIdentifier) {
|
||||
hiddenIdentifier.value = identifier;
|
||||
}
|
||||
if (hiddenPerson) {
|
||||
hiddenPerson.value = personId;
|
||||
}
|
||||
if (metaLine) {
|
||||
metaLine.textContent = core.titleCase(service) + " · " + identifier;
|
||||
}
|
||||
clearReplyTarget();
|
||||
closeSocket();
|
||||
lastTs = 0;
|
||||
thread.dataset.lastTs = "0";
|
||||
thread.innerHTML = "";
|
||||
ensureEmptyState("Loading messages...");
|
||||
applyTyping({ typing: false });
|
||||
poll(true);
|
||||
setupWebSocket();
|
||||
};
|
||||
|
||||
const bindContextSelectors = function () {
|
||||
if (platformSelect) {
|
||||
platformSelect.addEventListener("change", function () {
|
||||
const selected = platformSelect.options[platformSelect.selectedIndex];
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
const targetUrl = core.buildComposeUrl(
|
||||
renderMode,
|
||||
selected.value || "",
|
||||
selected.dataset.identifier || "",
|
||||
selected.dataset.person || ""
|
||||
);
|
||||
switchThreadContext(
|
||||
selected.value || "",
|
||||
selected.dataset.identifier || "",
|
||||
selected.dataset.person || "",
|
||||
targetUrl
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (contactSelect) {
|
||||
contactSelect.addEventListener("change", function () {
|
||||
const selected = contactSelect.options[contactSelect.selectedIndex];
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
const serviceMap = core.parseServiceMap(selected);
|
||||
const currentService = String(thread.dataset.service || "").toLowerCase();
|
||||
const availableServices = Object.keys(serviceMap);
|
||||
let selectedService = currentService || String(selected.dataset.service || "");
|
||||
let selectedIdentifier = String(serviceMap[selectedService] || "").trim();
|
||||
if (!selectedIdentifier) {
|
||||
selectedService = String(
|
||||
selected.dataset.service || selectedService
|
||||
).trim().toLowerCase();
|
||||
selectedIdentifier = String(serviceMap[selectedService] || "").trim();
|
||||
}
|
||||
if (!selectedIdentifier && availableServices.length) {
|
||||
selectedService = availableServices[0];
|
||||
selectedIdentifier = String(serviceMap[selectedService] || "").trim();
|
||||
}
|
||||
const targetUrl = core.buildComposeUrl(
|
||||
renderMode,
|
||||
selectedService,
|
||||
selectedIdentifier,
|
||||
selected.dataset.person || ""
|
||||
);
|
||||
switchThreadContext(
|
||||
selectedService,
|
||||
selectedIdentifier,
|
||||
selected.dataset.person || "",
|
||||
targetUrl
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const bindThreadEvents = function () {
|
||||
if (replyClearBtn) {
|
||||
replyClearBtn.addEventListener("click", function () {
|
||||
clearReplyTarget();
|
||||
textarea.focus();
|
||||
});
|
||||
}
|
||||
|
||||
thread.addEventListener("click", function (event) {
|
||||
const replyLink =
|
||||
event.target.closest && event.target.closest(".compose-reply-link");
|
||||
if (replyLink) {
|
||||
const replyRef = replyLink.closest(".compose-reply-ref");
|
||||
const targetId = String(
|
||||
(replyRef && replyRef.dataset && replyRef.dataset.replyTargetId) || ""
|
||||
).trim();
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
const targetRow = rowByMessageId(targetId);
|
||||
if (!targetRow) {
|
||||
return;
|
||||
}
|
||||
targetRow.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
flashReplyTarget(targetRow);
|
||||
return;
|
||||
}
|
||||
|
||||
const replyButton =
|
||||
event.target.closest && event.target.closest(".compose-reply-btn");
|
||||
if (!replyButton) {
|
||||
return;
|
||||
}
|
||||
const row = replyButton.closest(".compose-row");
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
setReplyTarget(row.dataset.messageId || "", row.dataset.replySnippet || "");
|
||||
textarea.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const init = function () {
|
||||
bindThreadEvents();
|
||||
bindContextSelectors();
|
||||
applyTyping(core.parseJsonSafe(panel.dataset.initialTyping || "{}", {}));
|
||||
ensureEmptyState();
|
||||
scrollToBottom(true);
|
||||
setupWebSocket();
|
||||
|
||||
state.pollTimer = setInterval(function () {
|
||||
if (!document.getElementById(config.panelId)) {
|
||||
if (window.GIAComposePanel) {
|
||||
window.GIAComposePanel.destroyPanel(config.panelId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
poll(false);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
return {
|
||||
clearReplyTarget: clearReplyTarget,
|
||||
init: init,
|
||||
poll: poll,
|
||||
queryParams: queryParams,
|
||||
setBeforeContextReset: function (callback) {
|
||||
beforeContextReset = callback;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
window.GIAComposePanelThread = {
|
||||
createController: createController,
|
||||
};
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
16
core/static/js/gridstack.min.js
vendored
16
core/static/js/gridstack.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
core/static/js/hyperscript.min.js
vendored
1
core/static/js/hyperscript.min.js
vendored
File diff suppressed because one or more lines are too long
2
core/static/js/jquery.min.js
vendored
2
core/static/js/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
2
core/static/js/magnet.min.js
vendored
2
core/static/js/magnet.min.js
vendored
File diff suppressed because one or more lines are too long
1182
core/static/js/workspace-shell.js
Normal file
1182
core/static/js/workspace-shell.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
||||
<svg width="800" height="800" viewBox="0 0 213.35 150.85" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(38.831 -7.4316)">
|
||||
<g transform="matrix(.99287 0 0 .99911 1.2367 -30.308)">
|
||||
<path d="m-32.645 113.03a115.16 122.5 0 0 1 99.73-61.25 115.16 122.5 0 0 1 99.73 61.25" fill="none" stroke="#f8fafc" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".99505" stroke-width="15.42" style="paint-order:markers fill stroke"/>
|
||||
<path d="m67.006 89.591a42.374 42.374 0 0 1 39.148 26.158 42.374 42.374 0 0 1-9.1855 46.179 42.374 42.374 0 0 1-46.179 9.1855 42.374 42.374 0 0 1-26.158-39.148" fill="none" stroke="#f8fafc" stroke-linecap="round" stroke-miterlimit="3.4" stroke-width="16.251"/>
|
||||
<circle cx="67.003" cy="131.96" r="13.151" fill="#740101"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 812 B |
@@ -1,9 +0,0 @@
|
||||
<svg width="800" height="800" viewBox="0 0 213.35 150.85" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(38.831 -7.4316)">
|
||||
<g transform="matrix(.99287 0 0 .99911 1.2367 -30.308)">
|
||||
<path d="m-32.645 113.03a115.16 122.5 0 0 1 99.73-61.25 115.16 122.5 0 0 1 99.73 61.25" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".99505" stroke-width="15.42" style="paint-order:markers fill stroke"/>
|
||||
<path d="m67.006 89.591a42.374 42.374 0 0 1 39.148 26.158 42.374 42.374 0 0 1-9.1855 46.179 42.374 42.374 0 0 1-46.179 9.1855 42.374 42.374 0 0 1-26.158-39.148" fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="3.4" stroke-width="16.251"/>
|
||||
<circle cx="67.003" cy="131.96" r="13.151" fill="#740101"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 806 B |
@@ -1,9 +0,0 @@
|
||||
<svg width="800" height="800" version="1.1" viewBox="0 0 211.7 211.7" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(38.55 -16.23)">
|
||||
<g transform="translate(0 .001548)">
|
||||
<path d="m-31.79 121.8a114.4 122.5 0 0 1 99.07-61.27 114.4 122.5 0 0 1 99.07 61.27" fill="none" stroke="#000" stroke-linejoin="round" stroke-opacity=".9951" stroke-width="15.37" style="paint-order:markers fill stroke"/>
|
||||
<path d="m67.28 98.38a42.09 42.39 0 0 1 38.89 26.17 42.09 42.39 0 0 1-9.125 46.2 42.09 42.39 0 0 1-45.87 9.189 42.09 42.39 0 0 1-25.98-39.16" fill="none" stroke="#000" stroke-miterlimit="3.4" stroke-width="16.2"/>
|
||||
<ellipse cx="67.28" cy="140.8" rx="13.06" ry="13.16" fill="#740101"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 719 B |
Reference in New Issue
Block a user