@nyaruka/temba-components 0.108.7 → 0.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/dist/static/svg/index.svg +1 -1
  3. package/dist/temba-components.js +602 -455
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/charcount/CharCount.js +4 -5
  6. package/out-tsc/src/charcount/CharCount.js.map +1 -1
  7. package/out-tsc/src/completion/Completion.js +27 -16
  8. package/out-tsc/src/completion/Completion.js.map +1 -1
  9. package/out-tsc/src/compose/Compose.js +259 -95
  10. package/out-tsc/src/compose/Compose.js.map +1 -1
  11. package/out-tsc/src/contacts/ContactChat.js +18 -16
  12. package/out-tsc/src/contacts/ContactChat.js.map +1 -1
  13. package/out-tsc/src/contacts/ContactTickets.js +1 -1
  14. package/out-tsc/src/contacts/ContactTickets.js.map +1 -1
  15. package/out-tsc/src/interfaces.js.map +1 -1
  16. package/out-tsc/src/list/ShortcutList.js +125 -0
  17. package/out-tsc/src/list/ShortcutList.js.map +1 -0
  18. package/out-tsc/src/list/TembaList.js +8 -5
  19. package/out-tsc/src/list/TembaList.js.map +1 -1
  20. package/out-tsc/src/options/Options.js +46 -35
  21. package/out-tsc/src/options/Options.js.map +1 -1
  22. package/out-tsc/src/select/Select.js +1 -1
  23. package/out-tsc/src/select/Select.js.map +1 -1
  24. package/out-tsc/src/store/Store.js +18 -3
  25. package/out-tsc/src/store/Store.js.map +1 -1
  26. package/out-tsc/src/tabpane/Tab.js +2 -0
  27. package/out-tsc/src/tabpane/Tab.js.map +1 -1
  28. package/out-tsc/src/tabpane/TabPane.js +27 -5
  29. package/out-tsc/src/tabpane/TabPane.js.map +1 -1
  30. package/out-tsc/src/textinput/TextInput.js +7 -2
  31. package/out-tsc/src/textinput/TextInput.js.map +1 -1
  32. package/out-tsc/src/utils/index.js.map +1 -1
  33. package/out-tsc/src/vectoricon/index.js +2 -2
  34. package/out-tsc/src/vectoricon/index.js.map +1 -1
  35. package/out-tsc/temba-modules.js +2 -0
  36. package/out-tsc/temba-modules.js.map +1 -1
  37. package/out-tsc/test/temba-compose.test.js +26 -18
  38. package/out-tsc/test/temba-compose.test.js.map +1 -1
  39. package/out-tsc/test/temba-contact-chat.test.js +27 -18
  40. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  41. package/package.json +1 -1
  42. package/screenshots/truth/compose/attachments-and-send-button.png +0 -0
  43. package/screenshots/truth/compose/attachments-no-send-button.png +0 -0
  44. package/screenshots/truth/compose/attachments-with-all-files-and-click-send.png +0 -0
  45. package/screenshots/truth/compose/attachments-with-all-files.png +0 -0
  46. package/screenshots/truth/compose/attachments-with-failure-files.png +0 -0
  47. package/screenshots/truth/compose/attachments-with-success-files-and-click-send.png +0 -0
  48. package/screenshots/truth/compose/attachments-with-success-files.png +0 -0
  49. package/screenshots/truth/compose/chatbox-attachments-counter-and-send-button.png +0 -0
  50. package/screenshots/truth/compose/chatbox-attachments-counter-no-send-button.png +0 -0
  51. package/screenshots/truth/compose/chatbox-attachments-no-counter-and-send-button.png +0 -0
  52. package/screenshots/truth/compose/chatbox-attachments-no-counter-no-send-button.png +0 -0
  53. package/screenshots/truth/compose/chatbox-counter-and-send-button.png +0 -0
  54. package/screenshots/truth/compose/chatbox-counter-no-send-button.png +0 -0
  55. package/screenshots/truth/compose/chatbox-no-counter-and-send-button.png +0 -0
  56. package/screenshots/truth/compose/chatbox-no-counter-no-send-button.png +0 -0
  57. package/screenshots/truth/compose/chatbox-no-text-attachments-with-all-files-and-click-send.png +0 -0
  58. package/screenshots/truth/compose/chatbox-no-text-attachments-with-all-files.png +0 -0
  59. package/screenshots/truth/compose/chatbox-no-text-attachments-with-failure-files.png +0 -0
  60. package/screenshots/truth/compose/chatbox-no-text-attachments-with-success-files-and-click-send.png +0 -0
  61. package/screenshots/truth/compose/chatbox-no-text-attachments-with-success-files.png +0 -0
  62. package/screenshots/truth/compose/chatbox-with-text-and-click-send.png +0 -0
  63. package/screenshots/truth/compose/chatbox-with-text-and-hit-enter.png +0 -0
  64. package/screenshots/truth/compose/chatbox-with-text-and-spaces.png +0 -0
  65. package/screenshots/truth/compose/chatbox-with-text-and-url.png +0 -0
  66. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files-and-click-send.png +0 -0
  67. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files-and-hit-enter.png +0 -0
  68. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files.png +0 -0
  69. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files-and-click-send.png +0 -0
  70. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files-and-hit-enter.png +0 -0
  71. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files.png +0 -0
  72. package/screenshots/truth/compose/chatbox-with-text-attachments-with-failure-files.png +0 -0
  73. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files-and-click-send.png +0 -0
  74. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files-and-hit-enter.png +0 -0
  75. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files.png +0 -0
  76. package/screenshots/truth/compose/chatbox-with-text-no-spaces.png +0 -0
  77. package/screenshots/truth/compose/chatbox-with-text.png +0 -0
  78. package/screenshots/truth/contacts/compose-attachments-no-text-failure.png +0 -0
  79. package/screenshots/truth/contacts/compose-attachments-no-text-success.png +0 -0
  80. package/screenshots/truth/contacts/compose-text-and-attachments-failure-attachments.png +0 -0
  81. package/screenshots/truth/contacts/compose-text-and-attachments-failure-generic.png +0 -0
  82. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text-and-attachments.png +0 -0
  83. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text.png +0 -0
  84. package/screenshots/truth/contacts/compose-text-and-attachments-success.png +0 -0
  85. package/screenshots/truth/contacts/compose-text-no-attachments-failure.png +0 -0
  86. package/screenshots/truth/contacts/compose-text-no-attachments-success.png +0 -0
  87. package/screenshots/truth/contacts/contact-active-default.png +0 -0
  88. package/screenshots/truth/contacts/contact-active-show-chatbox.png +0 -0
  89. package/screenshots/truth/counter/summary.png +0 -0
  90. package/screenshots/truth/counter/text.png +0 -0
  91. package/screenshots/truth/counter/unicode-variables.png +0 -0
  92. package/screenshots/truth/counter/unicode.png +0 -0
  93. package/screenshots/truth/counter/variable.png +0 -0
  94. package/src/charcount/CharCount.ts +4 -5
  95. package/src/completion/Completion.ts +33 -19
  96. package/src/compose/Compose.ts +289 -96
  97. package/src/contacts/ContactChat.ts +18 -16
  98. package/src/contacts/ContactTickets.ts +1 -1
  99. package/src/interfaces.ts +7 -0
  100. package/src/list/ShortcutList.ts +137 -0
  101. package/src/list/TembaList.ts +9 -6
  102. package/src/options/Options.ts +53 -44
  103. package/src/select/Select.ts +1 -1
  104. package/src/store/Store.ts +23 -4
  105. package/src/tabpane/Tab.ts +2 -0
  106. package/src/tabpane/TabPane.ts +28 -5
  107. package/src/textinput/TextInput.ts +9 -3
  108. package/src/utils/index.ts +8 -2
  109. package/src/vectoricon/index.ts +2 -2
  110. package/static/svg/index.svg +1 -1
  111. package/static/svg/work/traced/zap-fast.svg +1 -0
  112. package/static/svg/work/used/zap-fast.svg +3 -0
  113. package/temba-modules.ts +2 -0
  114. package/test/temba-compose.test.ts +28 -35
  115. package/test/temba-contact-chat.test.ts +28 -37
  116. package/test-assets/store/shortcuts.json +14 -0
  117. package/static/svg/work/traced/message-dots-circle.svg +0 -1
  118. package/static/svg/work/used/message-dots-circle.svg +0 -3
@@ -47,15 +47,14 @@ export class CharCount extends RapidElement {
47
47
  overflow: hidden;
48
48
  opacity: 0.3;
49
49
  transform: scale(0.7);
50
- box-shadow: var(--shadow);
50
+ box-shadow: var(--dropdown-shadow);
51
51
  transition: transform cubic-bezier(0.71, 0.18, 0.61, 1.33)
52
52
  var(--transition-speed);
53
53
  visibility: hidden;
54
- margin-top: var(--temba-charcount-summary-margin-top);
55
- right: var(--temba-charcount-summary-right);
56
- bottom: var(--temba-charcount-summary-bottom);
57
54
  text-align: left;
58
- position: var(--temba-charcount-summary-position);
55
+ position: fixed;
56
+ margin-left: -190px;
57
+ margin-top: 20px;
59
58
  z-index: 1000;
60
59
  }
61
60
 
@@ -116,6 +116,9 @@ export class Completion extends FormElement {
116
116
  @property({ type: Boolean })
117
117
  gsm: boolean;
118
118
 
119
+ @property({ type: Boolean })
120
+ disableCompletion: boolean;
121
+
119
122
  @property({ type: String })
120
123
  counter: string;
121
124
 
@@ -140,30 +143,32 @@ export class Completion extends FormElement {
140
143
  }
141
144
 
142
145
  private handleKeyUp(evt: KeyboardEvent) {
143
- // if we have options, ignore keys that are meant for them
144
- if (this.options && this.options.length > 0) {
145
- if (evt.key === 'ArrowUp' || evt.key === 'ArrowDown') {
146
- return;
147
- }
146
+ if (this.disableCompletion) {
147
+ // if we have options, ignore keys that are meant for them
148
+ if (this.options && this.options.length > 0) {
149
+ if (evt.key === 'ArrowUp' || evt.key === 'ArrowDown') {
150
+ return;
151
+ }
152
+
153
+ if (evt.ctrlKey) {
154
+ if (evt.key === 'n' || evt.key === 'p') {
155
+ return;
156
+ }
157
+ }
148
158
 
149
- if (evt.ctrlKey) {
150
- if (evt.key === 'n' || evt.key === 'p') {
159
+ if (
160
+ evt.key === 'Enter' ||
161
+ evt.key === 'Escape' ||
162
+ evt.key === 'Tab' ||
163
+ evt.key.startsWith('Control')
164
+ ) {
165
+ evt.stopPropagation();
166
+ evt.preventDefault();
151
167
  return;
152
168
  }
153
- }
154
169
 
155
- if (
156
- evt.key === 'Enter' ||
157
- evt.key === 'Escape' ||
158
- evt.key === 'Tab' ||
159
- evt.key.startsWith('Control')
160
- ) {
161
- evt.stopPropagation();
162
- evt.preventDefault();
163
- return;
170
+ this.executeQuery(evt.currentTarget as TextInput);
164
171
  }
165
-
166
- this.executeQuery(evt.currentTarget as TextInput);
167
172
  }
168
173
  }
169
174
 
@@ -172,10 +177,15 @@ export class Completion extends FormElement {
172
177
  }
173
178
 
174
179
  private executeQuery(ele: TextInput) {
180
+ if (this.disableCompletion) {
181
+ return;
182
+ }
183
+
175
184
  const store: Store = document.querySelector('temba-store');
176
185
  if (!ele.inputElement) {
177
186
  return;
178
187
  }
188
+
179
189
  const result = executeCompletionQuery(
180
190
  ele.inputElement,
181
191
  store,
@@ -232,6 +242,10 @@ export class Completion extends FormElement {
232
242
  }
233
243
  }
234
244
 
245
+ public getTextInput(): TextInput {
246
+ return this.textInputElement;
247
+ }
248
+
235
249
  public click() {
236
250
  super.click();
237
251
  const input = this.shadowRoot.querySelector('temba-textinput') as TextInput;
@@ -1,19 +1,20 @@
1
1
  import { TemplateResult, html, css } from 'lit';
2
2
  import { FormElement } from '../FormElement';
3
3
  import { property } from 'lit/decorators.js';
4
- import { Attachment, CustomEventType, Language } from '../interfaces';
4
+ import { Attachment, CustomEventType, Language, Shortcut } from '../interfaces';
5
5
  import { DEFAULT_MEDIA_ENDPOINT, getClasses } from '../utils';
6
6
  import { Completion } from '../completion/Completion';
7
7
  import { Select } from '../select/Select';
8
8
  import { TabPane } from '../tabpane/TabPane';
9
9
  import { MediaPicker } from '../mediapicker/MediaPicker';
10
10
  import { Tab } from '../tabpane/Tab';
11
+ import { TextInput } from '../textinput/TextInput';
12
+ import { ShortcutList } from '../list/ShortcutList';
11
13
 
12
14
  export class Compose extends FormElement {
13
15
  static get styles() {
14
16
  return css`
15
17
  :host {
16
- --textarea-min-height: var(--textarea-min-height, 4em);
17
18
  overflow: hidden;
18
19
  border-top-right-radius: var(--curvature);
19
20
  border-top-left-radius: var(--curvature);
@@ -32,13 +33,15 @@ export class Compose extends FormElement {
32
33
  flex-direction: column;
33
34
  justify-content: space-between;
34
35
  position: relative;
35
-
36
- border-radius: var(--curvature-widget);
36
+ overflow: hidden;
37
+ border-radius: var(--compose-curvature, var(--curvature-widget));
37
38
  background: var(--color-widget-bg);
38
39
  border: var(--compose-border, 1px solid var(--color-widget-border));
39
40
  transition: all ease-in-out var(--transition-speed);
40
41
  box-shadow: var(--compose-shadow, var(--widget-box-shadow));
41
42
  caret-color: var(--input-caret);
43
+ --color-widget-bg-focused: transparent;
44
+ --color-widget-bg: transparent;
42
45
  }
43
46
 
44
47
  .chatbox {
@@ -49,7 +52,10 @@ export class Compose extends FormElement {
49
52
  );
50
53
 
51
54
  --widget-box-shadow: none;
52
- padding: var(--compose-padding, 0px);
55
+ display: block;
56
+ flex-grow: 1;
57
+ --widget-box-shadow-focused: none;
58
+ --temba-textinput-padding: 1em 1em;
53
59
  }
54
60
 
55
61
  .actions {
@@ -58,9 +64,6 @@ export class Compose extends FormElement {
58
64
  align-items: center;
59
65
  padding: 0em;
60
66
  background: #f9f9f9;
61
- border-bottom-left-radius: var(--curvature);
62
- border-bottom-right-radius: var(--curvature);
63
- border-top: solid 1px var(--color-widget-border);
64
67
  }
65
68
 
66
69
  .actions-right {
@@ -86,6 +89,7 @@ export class Compose extends FormElement {
86
89
  .send-error {
87
90
  color: rgba(250, 0, 0, 0.75);
88
91
  font-size: var(--help-text-size);
92
+ padding: 0.5em;
89
93
  }
90
94
 
91
95
  .language {
@@ -98,12 +102,19 @@ export class Compose extends FormElement {
98
102
  display: flex;
99
103
  }
100
104
 
105
+ .gutter {
106
+ align-items: center;
107
+ display: flex;
108
+ margin: 0.5em;
109
+ }
110
+
101
111
  #send-button {
102
112
  margin: 0.3em;
103
113
  }
104
114
 
105
115
  temba-tabs {
106
116
  --focused-tab-color: #f4f4f4;
117
+ min-height: var(--compose-min-height, 13.5em);
107
118
  }
108
119
 
109
120
  .quick-replies {
@@ -111,14 +122,47 @@ export class Compose extends FormElement {
111
122
  }
112
123
 
113
124
  .optins {
114
- padding: 1em;
125
+ margin: 0.8em;
126
+ }
127
+
128
+ .templates {
129
+ margin: 0.8em;
115
130
  }
116
131
 
117
132
  .attachments {
133
+ min-height: 5em;
134
+ padding: 0.2em;
135
+ align-items: center;
136
+ display: flex;
137
+ background: #f9f9f9;
138
+ border-radius: var(--curvature);
139
+ margin: 0.6em;
140
+ margin-bottom: 0em;
141
+ }
142
+
143
+ .pane-bottom {
144
+ border: 0px solid red;
145
+ --color-placeholder: rgba(0, 0, 0, 0.2);
146
+ flex-grow: 99;
147
+ }
148
+
149
+ .shortcut-wrapper {
150
+ max-height: var(--shortcuts-height, 12em);
151
+ display: flex;
152
+ flex-direction: row;
153
+ align-items: stretch;
154
+ --options-block-shadow: none;
155
+ --curvature-widget: 0px;
156
+ --color-options-bg: #fff;
157
+ border-bottom: 1px solid var(--color-widget-border);
158
+ }
159
+
160
+ temba-shortcuts {
161
+ flex-grow: 1;
118
162
  }
119
163
 
120
- temba-template-editor {
121
- padding: 1em;
164
+ .quick-replies {
165
+ background: #f9f9f9;
122
166
  }
123
167
  `;
124
168
  }
@@ -239,6 +283,12 @@ export class Compose extends FormElement {
239
283
  @property({ type: Object })
240
284
  currentTab: Tab;
241
285
 
286
+ @property({ type: Boolean })
287
+ hasPendingText = false;
288
+
289
+ @property({ type: Object })
290
+ activeShortcut: Shortcut;
291
+
242
292
  public constructor() {
243
293
  super();
244
294
  }
@@ -253,6 +303,14 @@ export class Compose extends FormElement {
253
303
  private handleTabChanged() {
254
304
  const tabs = this.shadowRoot.querySelector('temba-tabs') as TabPane;
255
305
  this.currentTab = tabs.getCurrentTab();
306
+
307
+ if (this.currentTab && this.currentTab.name === 'Shortcuts') {
308
+ const shortcuts = this.shadowRoot.querySelector(
309
+ 'temba-shortcuts'
310
+ ) as ShortcutList;
311
+ shortcuts.filter = '';
312
+ }
313
+ this.setFocusOnChatbox();
256
314
  }
257
315
 
258
316
  public firstUpdated(changes: Map<string, any>): void {
@@ -354,14 +412,15 @@ export class Compose extends FormElement {
354
412
  if (completion) {
355
413
  window.setTimeout(() => {
356
414
  completion.focus();
357
- // this.resetTabs();
358
415
  }, 0);
359
416
  }
360
417
  }
361
418
  }
362
419
 
363
420
  public reset(): void {
364
- (this.shadowRoot.querySelector('.chatbox') as HTMLInputElement).value = '';
421
+ const completion = this.shadowRoot.querySelector('.chatbox') as Completion;
422
+ completion.textInputElement.value = '';
423
+ completion.value = '';
365
424
  this.initialText = '';
366
425
  this.currentText = '';
367
426
  this.currentQuickReplies = [];
@@ -380,8 +439,28 @@ export class Compose extends FormElement {
380
439
  }
381
440
 
382
441
  private handleChatboxChange(evt: Event) {
383
- const chatbox = evt.target as HTMLInputElement;
384
- this.currentText = chatbox.value;
442
+ const chatbox = evt.target as Completion;
443
+ const inputElement = chatbox.getTextInput().inputElement;
444
+
445
+ this.currentText = inputElement.value;
446
+ this.hasPendingText = inputElement.value.length > 0;
447
+
448
+ // is the last character a / and is it at the beginning of the line
449
+ const cursor = inputElement.selectionStart;
450
+ const text = inputElement.value;
451
+ const lineStart = text.lastIndexOf('\n', cursor - 1) + 1;
452
+ const line = text.substring(lineStart, cursor);
453
+
454
+ if (line.startsWith('/')) {
455
+ // switch to the shortcuts tab
456
+ const tabs = this.shadowRoot.querySelector('temba-tabs') as TabPane;
457
+ tabs.focusTab('Shortcuts');
458
+
459
+ const shortcuts = this.shadowRoot.querySelector(
460
+ 'temba-shortcuts'
461
+ ) as ShortcutList;
462
+ shortcuts.filter = line.substring(1);
463
+ }
385
464
  }
386
465
 
387
466
  public toggleButton() {
@@ -406,16 +485,68 @@ export class Compose extends FormElement {
406
485
  this.handleSend();
407
486
  }
408
487
 
409
- private handleSendEnter(evt: KeyboardEvent) {
410
- if (this.button) {
488
+ private getCurrentLine(): { text: string; index: number } {
489
+ const chatbox = this.shadowRoot.querySelector('.chatbox') as Completion;
490
+
491
+ const cursor = chatbox.getTextInput().inputElement.selectionStart - 1;
492
+ const text = chatbox.value;
493
+ const start = text.substring(0, cursor).lastIndexOf('\n') + 1;
494
+
495
+ let end = chatbox.value.indexOf('\n', start);
496
+ if (end === -1) {
497
+ end = chatbox.value.length;
498
+ }
499
+
500
+ return { text: chatbox.value.substring(start, end), index: start };
501
+ }
502
+
503
+ private handleKeyDown(evt: KeyboardEvent) {
504
+ const tabs = this.shadowRoot.querySelector('temba-tabs') as TabPane;
505
+ const num = parseInt(evt.key);
506
+ if (
507
+ !Number.isNaN(num) &&
508
+ num > 0 &&
509
+ evt.ctrlKey &&
510
+ evt.metaKey &&
511
+ num <= tabs.tabs.length
512
+ ) {
513
+ tabs.index = num - 1;
514
+ }
515
+
516
+ // if they type / as the first character in a line, switch to the shortcut
517
+ if (evt.key === '/' && this.currentTab.name !== 'Shortcuts') {
518
+ const line = this.getCurrentLine();
519
+ const text = line.text.trim();
520
+ if (text.trim().length === 1) {
521
+ evt.preventDefault();
522
+ tabs.index = tabs.tabs.findIndex((tab) => tab.name === 'Shortcuts');
523
+ }
524
+ } else if (evt.key === 'Backspace') {
525
+ const line = this.getCurrentLine();
526
+ const text = line.text;
527
+ if (text === '/') {
528
+ tabs.index = tabs.tabs.findIndex((tab) => tab.name === 'Reply');
529
+ }
530
+ }
531
+
532
+ if (this.currentTab.name === 'Shortcuts') {
411
533
  if (evt.key === 'Enter' && !evt.shiftKey) {
412
- if (this.completion) {
413
- const chat = evt.target as Completion;
414
- if (!chat.hasVisibleOptions()) {
534
+ return;
535
+ }
536
+ }
537
+
538
+ if (this.button) {
539
+ if (evt.key === 'Enter') {
540
+ if (!evt.shiftKey) {
541
+ evt.preventDefault();
542
+ if (this.completion) {
543
+ const chat = evt.target as Completion;
544
+ if (!chat.hasVisibleOptions()) {
545
+ this.handleSend();
546
+ }
547
+ } else {
415
548
  this.handleSend();
416
549
  }
417
- } else {
418
- this.handleSend();
419
550
  }
420
551
  }
421
552
  }
@@ -435,7 +566,7 @@ export class Compose extends FormElement {
435
566
  }
436
567
 
437
568
  public resetTabs() {
438
- (this.shadowRoot.querySelector('temba-tabs') as TabPane).index = -1;
569
+ (this.shadowRoot.querySelector('temba-tabs') as TabPane).index = 0;
439
570
  }
440
571
 
441
572
  public render(): TemplateResult {
@@ -463,43 +594,12 @@ export class Compose extends FormElement {
463
594
  </temba-select>`
464
595
  : null}
465
596
  <div class="container">
466
- ${this.chatbox ? html`${this.getChatbox()}` : null}
467
597
  <div class="items actions">${this.getActions()}</div>
468
598
  </div>
469
599
  </temba-field>
470
600
  `;
471
601
  }
472
602
 
473
- private getChatbox(): TemplateResult {
474
- if (this.completion) {
475
- return html`<temba-completion
476
- class="chatbox"
477
- .value=${this.initialText}
478
- gsm
479
- textarea
480
- ?autogrow=${this.autogrow}
481
- maxlength=${this.maxLength}
482
- @change=${this.handleChatboxChange}
483
- @keydown=${this.handleSendEnter}
484
- placeholder="Write something here"
485
- >
486
- </temba-completion>`;
487
- } else {
488
- return html`<temba-textinput
489
- class="chatbox"
490
- gsm
491
- textarea
492
- ?autogrow=${this.autogrow}
493
- maxlength=${this.maxLength}
494
- .value=${this.initialText}
495
- @change=${this.handleChatboxChange}
496
- @keydown=${this.handleSendEnter}
497
- placeholder="Write something here"
498
- >
499
- </temba-textinput>`;
500
- }
501
- }
502
-
503
603
  private handleTemplateChanged(evt: CustomEvent) {
504
604
  this.currentTemplate = evt.detail.template;
505
605
  this.locale = evt.detail.translation?.locale;
@@ -510,6 +610,48 @@ export class Compose extends FormElement {
510
610
  this.variables = [...evt.detail.variables];
511
611
  }
512
612
 
613
+ public getTextInput(): TextInput {
614
+ return (
615
+ this.shadowRoot.querySelector('.chatbox') as Completion
616
+ ).getTextInput();
617
+ }
618
+
619
+ public handleShortcutSelection(event: CustomEvent) {
620
+ this.activeShortcut = event.detail.selected;
621
+ const line = this.getCurrentLine();
622
+ const chatbox = this.getTextInput();
623
+
624
+ const originalText = chatbox.value;
625
+
626
+ if (line.text.startsWith('/')) {
627
+ const newText =
628
+ originalText.substring(0, line.index) +
629
+ this.activeShortcut.text +
630
+ originalText.substring(line.index + line.text.length);
631
+
632
+ chatbox.updateValue(newText);
633
+
634
+ // set our cursor to the end of the shortcut
635
+ const cursor = line.index + this.activeShortcut.text.length;
636
+ chatbox.inputElement.setSelectionRange(cursor, cursor);
637
+ } else {
638
+ // add the text where the cursor is
639
+ const cursor = chatbox.inputElement.selectionStart;
640
+ const newText =
641
+ originalText.substring(0, cursor) +
642
+ this.activeShortcut.text +
643
+ originalText.substring(cursor);
644
+ chatbox.updateValue(newText);
645
+
646
+ // set the cursor to the end of the shortcut text
647
+ const newCursor = cursor + this.activeShortcut.text.length;
648
+ chatbox.inputElement.setSelectionRange(newCursor, newCursor);
649
+ }
650
+
651
+ const tabs = this.shadowRoot.querySelector('temba-tabs') as TabPane;
652
+ tabs.index = tabs.tabs.findIndex((tab) => tab.name === 'Reply');
653
+ }
654
+
513
655
  private getActions(): TemplateResult {
514
656
  const showOptins = this.optIns && this.isBaseLanguage();
515
657
  const showTemplates = this.templates && this.isBaseLanguage();
@@ -517,17 +659,22 @@ export class Compose extends FormElement {
517
659
  <temba-tabs
518
660
  embedded
519
661
  focusedname
520
- bottom
521
- unselect
662
+ index="0"
522
663
  @temba-context-changed=${this.handleTabChanged}
523
664
  refresh="${(this.currentAttachments || []).length}|${this.index}|${this
524
665
  .currentQuickReplies.length}|${showOptins}|${this
525
666
  .currentOptin}|${showTemplates}|${this.currentTemplate}"
526
667
  >
668
+ <temba-tab
669
+ name="Reply"
670
+ icon="message"
671
+ selectionBackground="#fff"
672
+ ></temba-tab>
527
673
  ${this.attachments
528
674
  ? html`<temba-tab
529
675
  name="Attachments"
530
676
  icon="attachment"
677
+ selectionBackground="#fff"
531
678
  .count=${(this.currentAttachments || []).length}
532
679
  >
533
680
  <div class="items attachments">
@@ -544,9 +691,11 @@ export class Compose extends FormElement {
544
691
  ? html`<temba-tab
545
692
  name="Quick Replies"
546
693
  icon="quick_replies"
694
+ selectionBackground="#fff"
547
695
  .count=${this.currentQuickReplies.length}
548
696
  >
549
697
  <temba-select
698
+ class="quick-replies"
550
699
  @change=${this.handleQuickReplyChange}
551
700
  .values=${this.currentQuickReplies}
552
701
  class="quick-replies"
@@ -558,48 +707,90 @@ export class Compose extends FormElement {
558
707
  ></temba-select>
559
708
  </temba-tab>`
560
709
  : null}
561
- <temba-tab
562
- name="Opt-in"
563
- icon="channel_fba"
564
- ?hidden=${!showOptins}
565
- ?checked=${this.currentOptin.length > 0}
566
- >
567
- <temba-select
568
- @change=${this.handleOptInChange}
569
- .values=${this.currentOptin}
570
- endpoint="${this.optinEndpoint}"
571
- class="optins"
572
- searchable
573
- clearable
574
- placeholder="Select an opt-in to use for Facebook (optional)"
575
- ></temba-select>
576
- </temba-tab>
710
+ ${showOptins
711
+ ? html`<temba-tab
712
+ name="Opt-in"
713
+ icon="channel_fba"
714
+ selectionBackground="#fff"
715
+ ?hidden=${!showOptins}
716
+ ?checked=${this.currentOptin.length > 0}
717
+ >
718
+ <temba-select
719
+ @change=${this.handleOptInChange}
720
+ .values=${this.currentOptin}
721
+ endpoint="${this.optinEndpoint}"
722
+ class="optins"
723
+ searchable
724
+ clearable
725
+ placeholder="Select an opt-in to use for Facebook (optional)"
726
+ ></temba-select>
727
+ </temba-tab>`
728
+ : null}
729
+ ${showTemplates
730
+ ? html`<temba-tab
731
+ name="Template"
732
+ icon="channel_wa"
733
+ selectionBackground="#fff"
734
+ ?alert=${this.errors &&
735
+ this.errors.find((error) => error.includes('template'))}
736
+ ?hidden=${!showTemplates}
737
+ ?checked=${this.currentTemplate}
738
+ >
739
+ <temba-template-editor
740
+ class="templates"
741
+ @temba-context-changed=${this.handleTemplateChanged}
742
+ @temba-content-changed=${this.handleTemplateVariablesChanged}
743
+ template=${this.template}
744
+ variables=${JSON.stringify(this.variables)}
745
+ url=${this.templateEndpoint}
746
+ lang=${this.currentLanguage}
747
+ >
748
+ </temba-template-editor>
749
+ </temba-tab>`
750
+ : null}
577
751
 
578
- <temba-tab
579
- name="Template"
580
- icon="channel_wa"
581
- ?alert=${this.errors &&
582
- this.errors.find((error) => error.includes('template'))}
583
- ?hidden=${!showTemplates}
584
- ?checked=${this.currentTemplate}
585
- >
586
- <temba-template-editor
587
- @temba-context-changed=${this.handleTemplateChanged}
588
- @temba-content-changed=${this.handleTemplateVariablesChanged}
589
- template=${this.template}
590
- variables=${JSON.stringify(this.variables)}
591
- url=${this.templateEndpoint}
592
- lang=${this.currentLanguage}
593
- >
594
- </temba-template-editor>
752
+ <!--temba-tab
753
+ name="Note"
754
+ icon="notes"
755
+ activityColor="#ffbd00"
756
+ selectionBackground="#fff9c2"
757
+ borderColor="#ebdf6f"
758
+ ></temba-tab-->
759
+
760
+ <temba-tab name="Shortcuts" icon="shortcut" selectionBackground="#fff">
761
+ <div class="shortcut-wrapper">
762
+ <temba-shortcuts
763
+ @temba-selection=${this.handleShortcutSelection}
764
+ ></temba-shortcuts>
765
+ </div>
595
766
  </temba-tab>
596
767
 
597
768
  <div slot="tab-right" class="top-right">
769
+ ${this.counter ? this.getCounter() : null}
770
+ </div>
771
+
772
+ <div
773
+ slot="pane-bottom"
774
+ class="pane-bottom ${this.hasPendingText ? 'pending' : ''}"
775
+ >
776
+ ${this.chatbox
777
+ ? html`<temba-completion
778
+ class="chatbox"
779
+ .value=${this.initialText}
780
+ gsm
781
+ textarea
782
+ ?disableCompletion=${!this.completion}
783
+ ?autogrow=${this.autogrow}
784
+ maxlength=${this.maxLength}
785
+ @change=${this.handleChatboxChange}
786
+ @keydown=${this.handleKeyDown}
787
+ placeholder="Write something here"
788
+ >
789
+ </temba-completion>`
790
+ : null}
598
791
  ${this.buttonError
599
792
  ? html`<div class="send-error">${this.buttonError}</div>`
600
793
  : null}
601
- ${this.counter ? this.getCounter() : null}
602
- ${this.button ? this.getButton() : null}
603
794
  </div>
604
795
  </temba-tabs>
605
796
  `;
@@ -612,11 +803,13 @@ export class Compose extends FormElement {
612
803
  }
613
804
 
614
805
  private getButton(): TemplateResult {
615
- return html` <temba-button
616
- id="send-button"
617
- name=${this.buttonName}
806
+ return html`<temba-icon
807
+ tabindex="1"
808
+ class="send-icon"
809
+ name="send"
810
+ size="1"
811
+ clickable
618
812
  @click=${this.handleSendClick}
619
- ?disabled=${this.buttonDisabled}
620
- ></temba-button>`;
813
+ ></temba-icon>`;
621
814
  }
622
815
  }