@nyaruka/temba-components 0.134.3 → 0.134.4
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.
- package/CHANGELOG.md +5 -13
- package/dist/temba-components.js +200 -193
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +127 -52
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +3 -3
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
- package/src/display/Chat.ts +175 -75
- package/src/live/ContactChat.ts +3 -3
- package/test-assets/contacts/history.json +1 -1
package/src/display/Chat.ts
CHANGED
|
@@ -48,6 +48,17 @@ export enum MessageType {
|
|
|
48
48
|
Note = 'note'
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export type GroupReason =
|
|
52
|
+
| 'time_elapsed'
|
|
53
|
+
| 'new_author'
|
|
54
|
+
| 'new_type'
|
|
55
|
+
| 'initial';
|
|
56
|
+
|
|
57
|
+
export interface MessageGroup {
|
|
58
|
+
messages: string[];
|
|
59
|
+
reason: GroupReason;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
export interface ObjectReference {
|
|
52
63
|
uuid: string;
|
|
53
64
|
name: string;
|
|
@@ -76,7 +87,7 @@ export interface Msg {
|
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
export interface ContactEvent {
|
|
79
|
-
uuid
|
|
90
|
+
uuid?: string;
|
|
80
91
|
type: string;
|
|
81
92
|
created_on: Date;
|
|
82
93
|
_user?: User;
|
|
@@ -96,6 +107,7 @@ export interface MsgEvent extends ContactEvent {
|
|
|
96
107
|
by_contact: boolean;
|
|
97
108
|
user: { name: string; uuid: string };
|
|
98
109
|
};
|
|
110
|
+
_logs_url?: string;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
const TIME_FORMAT = { hour: 'numeric', minute: '2-digit' } as any;
|
|
@@ -107,6 +119,14 @@ const VERBOSE_FORMAT = {
|
|
|
107
119
|
hour: 'numeric',
|
|
108
120
|
minute: '2-digit'
|
|
109
121
|
} as any;
|
|
122
|
+
const VERBOSE_FORMAT_WITH_YEAR = {
|
|
123
|
+
weekday: undefined,
|
|
124
|
+
year: 'numeric',
|
|
125
|
+
month: 'short',
|
|
126
|
+
day: 'numeric',
|
|
127
|
+
hour: 'numeric',
|
|
128
|
+
minute: '2-digit'
|
|
129
|
+
} as any;
|
|
110
130
|
|
|
111
131
|
export class Chat extends RapidElement {
|
|
112
132
|
static get styles() {
|
|
@@ -185,6 +205,16 @@ export class Chat extends RapidElement {
|
|
|
185
205
|
padding-top: 0;
|
|
186
206
|
}
|
|
187
207
|
|
|
208
|
+
.group-reason {
|
|
209
|
+
text-align: center;
|
|
210
|
+
font-size: 0.75em;
|
|
211
|
+
color: #999;
|
|
212
|
+
margin-bottom: 1em;
|
|
213
|
+
margin-top: 0.5em;
|
|
214
|
+
padding: 0.5em 1em;
|
|
215
|
+
font-style: italic;
|
|
216
|
+
}
|
|
217
|
+
|
|
188
218
|
.row {
|
|
189
219
|
display: flex;
|
|
190
220
|
flex-direction: row;
|
|
@@ -229,6 +259,7 @@ export class Chat extends RapidElement {
|
|
|
229
259
|
.incoming .row {
|
|
230
260
|
flex-direction: row-reverse;
|
|
231
261
|
margin-left: 1em;
|
|
262
|
+
margin-right: 1em;
|
|
232
263
|
}
|
|
233
264
|
|
|
234
265
|
.bubble {
|
|
@@ -289,7 +320,8 @@ export class Chat extends RapidElement {
|
|
|
289
320
|
color: #ad47479a;
|
|
290
321
|
}
|
|
291
322
|
|
|
292
|
-
.message {
|
|
323
|
+
.message-text {
|
|
324
|
+
white-space: pre-wrap;
|
|
293
325
|
margin-bottom: 0.5em;
|
|
294
326
|
line-height: 1.2em;
|
|
295
327
|
word-break: break-word;
|
|
@@ -380,8 +412,6 @@ export class Chat extends RapidElement {
|
|
|
380
412
|
display: flex;
|
|
381
413
|
flex-direction: column;
|
|
382
414
|
align-items: flex-start;
|
|
383
|
-
margin-top: -1em;
|
|
384
|
-
padding-top: 1em;
|
|
385
415
|
}
|
|
386
416
|
|
|
387
417
|
.scroll-at-top.messages:before {
|
|
@@ -532,8 +562,6 @@ export class Chat extends RapidElement {
|
|
|
532
562
|
rgba(0, 0, 0, 0.2) 0px 1px 2px 0px;
|
|
533
563
|
border: 1px solid #f3f3f3;
|
|
534
564
|
opacity: 0;
|
|
535
|
-
transform: scale(0.7);
|
|
536
|
-
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
|
|
537
565
|
z-index: 2;
|
|
538
566
|
}
|
|
539
567
|
|
|
@@ -550,9 +578,9 @@ export class Chat extends RapidElement {
|
|
|
550
578
|
}
|
|
551
579
|
|
|
552
580
|
.bubble-wrap:hover .popup {
|
|
553
|
-
transform: translateY(-120%);
|
|
554
581
|
opacity: 1;
|
|
555
582
|
transition-delay: 1s;
|
|
583
|
+
top: -35px;
|
|
556
584
|
}
|
|
557
585
|
|
|
558
586
|
.new-message-notification {
|
|
@@ -589,7 +617,7 @@ export class Chat extends RapidElement {
|
|
|
589
617
|
}
|
|
590
618
|
|
|
591
619
|
@property({ type: Array })
|
|
592
|
-
messageGroups:
|
|
620
|
+
messageGroups: MessageGroup[] = [];
|
|
593
621
|
|
|
594
622
|
@property({ type: Boolean })
|
|
595
623
|
fetching = false;
|
|
@@ -713,20 +741,43 @@ export class Chat extends RapidElement {
|
|
|
713
741
|
return this.msgMap.has(msg.uuid);
|
|
714
742
|
}
|
|
715
743
|
|
|
716
|
-
private isSameGroup(
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
744
|
+
private isSameGroup(
|
|
745
|
+
msg1: ContactEvent,
|
|
746
|
+
msg2: ContactEvent,
|
|
747
|
+
lastTimeElapsedDate?: Date
|
|
748
|
+
): { same: boolean; reason?: GroupReason } {
|
|
749
|
+
if (!msg1 || !msg2) {
|
|
750
|
+
return { same: true };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// for type equivalence, treat all non-message types as the same
|
|
754
|
+
const isMsg1 = msg1.type === 'msg_created' || msg1.type === 'msg_received';
|
|
755
|
+
const isMsg2 = msg2.type === 'msg_created' || msg2.type === 'msg_received';
|
|
756
|
+
const typeMatch =
|
|
757
|
+
isMsg1 && isMsg2 ? msg1.type === msg2.type : isMsg1 === isMsg2;
|
|
758
|
+
|
|
759
|
+
// check time first - if BATCH_TIME_WINDOW has passed since last time_elapsed reason
|
|
760
|
+
const timeToCheck = lastTimeElapsedDate || msg1.created_on;
|
|
761
|
+
if (
|
|
762
|
+
Math.abs(msg2.created_on.getTime() - timeToCheck.getTime()) >=
|
|
763
|
+
BATCH_TIME_WINDOW
|
|
764
|
+
) {
|
|
765
|
+
return { same: false, reason: 'time_elapsed' };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (!typeMatch) {
|
|
769
|
+
return { same: false, reason: 'new_type' };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// only check author for message types
|
|
773
|
+
if (isMsg1 && isMsg2 && msg1._user?.name !== msg2._user?.name) {
|
|
774
|
+
return { same: false, reason: 'new_author' };
|
|
724
775
|
}
|
|
725
776
|
|
|
726
|
-
return
|
|
777
|
+
return { same: true };
|
|
727
778
|
}
|
|
728
779
|
|
|
729
|
-
private insertGroups(newGroups:
|
|
780
|
+
private insertGroups(newGroups: MessageGroup[], append = false) {
|
|
730
781
|
if (!append) {
|
|
731
782
|
newGroups.reverse();
|
|
732
783
|
}
|
|
@@ -737,12 +788,13 @@ export class Chat extends RapidElement {
|
|
|
737
788
|
this.messageGroups[append ? 0 : this.messageGroups.length - 1];
|
|
738
789
|
|
|
739
790
|
if (group) {
|
|
740
|
-
const lastMsgId = group[group.length - 1];
|
|
791
|
+
const lastMsgId = group.messages[group.messages.length - 1];
|
|
741
792
|
const lastMsg = this.msgMap.get(lastMsgId);
|
|
742
|
-
const newMsg = this.msgMap.get(newGroup[0]);
|
|
793
|
+
const newMsg = this.msgMap.get(newGroup.messages[0]);
|
|
743
794
|
// if our message belongs to the previous group, in we go
|
|
744
|
-
|
|
745
|
-
|
|
795
|
+
const groupCheck = this.isSameGroup(lastMsg, newMsg);
|
|
796
|
+
if (groupCheck.same) {
|
|
797
|
+
group.messages.push(...newGroup.messages);
|
|
746
798
|
} else {
|
|
747
799
|
// otherwise, just add our entire group as a new one
|
|
748
800
|
if (append) {
|
|
@@ -763,18 +815,31 @@ export class Chat extends RapidElement {
|
|
|
763
815
|
this.requestUpdate('messageGroups');
|
|
764
816
|
}
|
|
765
817
|
|
|
766
|
-
private groupMessages(msgIds: string[]):
|
|
818
|
+
private groupMessages(msgIds: string[]): MessageGroup[] {
|
|
767
819
|
// group our messages by origin and user
|
|
768
|
-
const groups = [];
|
|
769
|
-
let lastGroup =
|
|
770
|
-
let lastMsg = null;
|
|
820
|
+
const groups: MessageGroup[] = [];
|
|
821
|
+
let lastGroup: MessageGroup = null;
|
|
822
|
+
let lastMsg: ContactEvent = null;
|
|
823
|
+
let lastTimeElapsedDate: Date = null;
|
|
824
|
+
|
|
771
825
|
for (const msgId of msgIds) {
|
|
772
826
|
const msg = this.msgMap.get(msgId);
|
|
773
|
-
|
|
774
|
-
|
|
827
|
+
const groupCheck = this.isSameGroup(msg, lastMsg, lastTimeElapsedDate);
|
|
828
|
+
|
|
829
|
+
if (!groupCheck.same || !lastGroup) {
|
|
830
|
+
lastGroup = {
|
|
831
|
+
messages: [],
|
|
832
|
+
reason: groupCheck.reason || 'initial'
|
|
833
|
+
};
|
|
775
834
|
groups.push(lastGroup);
|
|
835
|
+
|
|
836
|
+
// track when we last broke for time_elapsed
|
|
837
|
+
if (groupCheck.reason === 'time_elapsed') {
|
|
838
|
+
lastTimeElapsedDate = msg.created_on;
|
|
839
|
+
}
|
|
776
840
|
}
|
|
777
|
-
|
|
841
|
+
|
|
842
|
+
lastGroup.messages.push(msgId);
|
|
778
843
|
lastMsg = msg;
|
|
779
844
|
}
|
|
780
845
|
return groups;
|
|
@@ -822,48 +887,30 @@ export class Chat extends RapidElement {
|
|
|
822
887
|
this.scrollToBottom();
|
|
823
888
|
}
|
|
824
889
|
|
|
890
|
+
private getReasonLabel(reason: GroupReason): string {
|
|
891
|
+
switch (reason) {
|
|
892
|
+
case 'new_author':
|
|
893
|
+
return '👤 Different author';
|
|
894
|
+
case 'new_type':
|
|
895
|
+
return '🔄 Message type changed';
|
|
896
|
+
case 'time_elapsed':
|
|
897
|
+
case 'initial':
|
|
898
|
+
default:
|
|
899
|
+
return '';
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
825
903
|
private renderMessageGroup(
|
|
826
|
-
|
|
904
|
+
group: MessageGroup,
|
|
827
905
|
idx: number,
|
|
828
|
-
|
|
829
|
-
): TemplateResult {
|
|
906
|
+
lastShownTimestamp: Date | null
|
|
907
|
+
): { html: TemplateResult; timestamp: Date | null } {
|
|
830
908
|
const today = new Date();
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
let prevMsg: ContactEvent;
|
|
834
|
-
if (idx > 0) {
|
|
835
|
-
const lastGroup = groups[idx - 1];
|
|
836
|
-
if (lastGroup && lastGroup.length > 0) {
|
|
837
|
-
prevMsg = this.msgMap.get(lastGroup[0]);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
909
|
+
const msgIds = group.messages;
|
|
840
910
|
|
|
841
911
|
const mostRecentId = msgIds[msgIds.length - 1];
|
|
842
912
|
const currentMsg = this.msgMap.get(mostRecentId);
|
|
843
913
|
|
|
844
|
-
let timeDisplay = null;
|
|
845
|
-
if (
|
|
846
|
-
prevMsg &&
|
|
847
|
-
!this.isSameGroup(prevMsg, currentMsg) &&
|
|
848
|
-
(Math.abs(
|
|
849
|
-
currentMsg.created_on.getTime() - prevMsg.created_on.getTime()
|
|
850
|
-
) > BATCH_TIME_WINDOW ||
|
|
851
|
-
idx === groups.length - 1)
|
|
852
|
-
) {
|
|
853
|
-
if (
|
|
854
|
-
today.getDate() !== prevMsg.created_on.getDate() ||
|
|
855
|
-
prevMsg.created_on.getDate() !== currentMsg.created_on.getDate()
|
|
856
|
-
) {
|
|
857
|
-
timeDisplay = html`<div class="time ${firstGroup ? 'first' : ''}">
|
|
858
|
-
${prevMsg.created_on.toLocaleTimeString(undefined, VERBOSE_FORMAT)}
|
|
859
|
-
</div>`;
|
|
860
|
-
} else {
|
|
861
|
-
timeDisplay = html`<div class="time ${firstGroup ? 'first' : ''}">
|
|
862
|
-
${prevMsg.created_on.toLocaleTimeString(undefined, TIME_FORMAT)}
|
|
863
|
-
</div>`;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
914
|
const incoming = this.agent
|
|
868
915
|
? currentMsg.type !== 'msg_received'
|
|
869
916
|
: currentMsg.type === 'msg_received';
|
|
@@ -878,8 +925,48 @@ export class Chat extends RapidElement {
|
|
|
878
925
|
|
|
879
926
|
const isSystem = !currentMsg._user?.uuid;
|
|
880
927
|
|
|
881
|
-
|
|
882
|
-
|
|
928
|
+
const reasonLabel = this.getReasonLabel(group.reason);
|
|
929
|
+
const showReason = false; // reasonLabel && idx > 0;
|
|
930
|
+
|
|
931
|
+
// determine if we should show a timestamp
|
|
932
|
+
// use the first message in the group (oldest) for the timestamp
|
|
933
|
+
const firstMsgId = msgIds[0];
|
|
934
|
+
const firstMsg = this.msgMap.get(firstMsgId);
|
|
935
|
+
|
|
936
|
+
// check if we should show a timestamp based on the last shown timestamp
|
|
937
|
+
let showTimeForReason = false;
|
|
938
|
+
let newLastShownTimestamp = lastShownTimestamp;
|
|
939
|
+
|
|
940
|
+
if (idx > 0) {
|
|
941
|
+
if (lastShownTimestamp) {
|
|
942
|
+
const timeSinceLastShown = Math.abs(
|
|
943
|
+
firstMsg.created_on.getTime() - lastShownTimestamp.getTime()
|
|
944
|
+
);
|
|
945
|
+
showTimeForReason = timeSinceLastShown >= BATCH_TIME_WINDOW;
|
|
946
|
+
} else {
|
|
947
|
+
// no previous timestamp, check against previous group
|
|
948
|
+
showTimeForReason = group.reason === 'time_elapsed';
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
let timeForReason = null;
|
|
953
|
+
if (showTimeForReason) {
|
|
954
|
+
newLastShownTimestamp = firstMsg.created_on;
|
|
955
|
+
|
|
956
|
+
const isDifferentYear =
|
|
957
|
+
today.getFullYear() !== firstMsg.created_on.getFullYear();
|
|
958
|
+
const format = isDifferentYear
|
|
959
|
+
? VERBOSE_FORMAT_WITH_YEAR
|
|
960
|
+
: today.getDate() !== firstMsg.created_on.getDate()
|
|
961
|
+
? VERBOSE_FORMAT
|
|
962
|
+
: TIME_FORMAT;
|
|
963
|
+
|
|
964
|
+
timeForReason = html`<div class="time time-elapsed">
|
|
965
|
+
${firstMsg.created_on.toLocaleTimeString(undefined, format)}
|
|
966
|
+
</div>`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const resultHtml = html`
|
|
883
970
|
<div class="block ${incoming ? 'incoming' : 'outgoing'}">
|
|
884
971
|
<div class="group-messages" style="flex-grow:1">
|
|
885
972
|
${repeat(
|
|
@@ -916,7 +1003,13 @@ export class Chat extends RapidElement {
|
|
|
916
1003
|
</div>`
|
|
917
1004
|
: null}
|
|
918
1005
|
</div>
|
|
1006
|
+
${showReason
|
|
1007
|
+
? html`<div class="group-reason">${reasonLabel}</div>`
|
|
1008
|
+
: null}
|
|
1009
|
+
${timeForReason}
|
|
919
1010
|
`;
|
|
1011
|
+
|
|
1012
|
+
return { html: resultHtml, timestamp: newLastShownTimestamp };
|
|
920
1013
|
}
|
|
921
1014
|
|
|
922
1015
|
private renderMessage(event: ContactEvent, name = null): TemplateResult {
|
|
@@ -967,7 +1060,7 @@ export class Chat extends RapidElement {
|
|
|
967
1060
|
${message.msg.text
|
|
968
1061
|
? html`<div class="bubble">
|
|
969
1062
|
${name ? html`<div class="name">${name}</div>` : null}
|
|
970
|
-
<div class="message
|
|
1063
|
+
<div class="message-text">${message.msg.text}</div>
|
|
971
1064
|
</div>`
|
|
972
1065
|
: null}
|
|
973
1066
|
|
|
@@ -1006,16 +1099,23 @@ export class Chat extends RapidElement {
|
|
|
1006
1099
|
>
|
|
1007
1100
|
<div class="scroll" @scroll=${this.handleScroll}>
|
|
1008
1101
|
${this.messageGroups
|
|
1009
|
-
?
|
|
1010
|
-
|
|
1011
|
-
(
|
|
1012
|
-
|
|
1013
|
-
|
|
1102
|
+
? (() => {
|
|
1103
|
+
let lastTimestamp: Date | null = null;
|
|
1104
|
+
// process from oldest to newest (high index to low index)
|
|
1105
|
+
// to establish logical time groupings going forward in time
|
|
1106
|
+
const results = [];
|
|
1107
|
+
for (let idx = this.messageGroups.length - 1; idx >= 0; idx--) {
|
|
1108
|
+
const msgGroup = this.messageGroups[idx];
|
|
1109
|
+
const result = this.renderMessageGroup(
|
|
1014
1110
|
msgGroup,
|
|
1015
1111
|
idx,
|
|
1016
|
-
|
|
1017
|
-
)
|
|
1018
|
-
|
|
1112
|
+
lastTimestamp
|
|
1113
|
+
);
|
|
1114
|
+
lastTimestamp = result.timestamp;
|
|
1115
|
+
results.unshift(result.html); // add to front since we're going backwards
|
|
1116
|
+
}
|
|
1117
|
+
return results;
|
|
1118
|
+
})()
|
|
1019
1119
|
: null}
|
|
1020
1120
|
|
|
1021
1121
|
<temba-loading
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -161,9 +161,9 @@ export const renderTicketAction = (
|
|
|
161
161
|
|
|
162
162
|
const actionNote = event.note
|
|
163
163
|
? html`<div
|
|
164
|
-
style="width:85%; background: #fffac3; padding: 1em;margin-bottom: 1em
|
|
164
|
+
style="width:85%; background: #fffac3; padding: 1em;margin-bottom: 1em;margin-top:1em; border: 1px solid #ffe97f;border-radius: var(--curvature);line-height: 1.2em; word-break: break-word;"
|
|
165
165
|
>
|
|
166
|
-
<div style="color: #8e830fff; font-size: 1em;margin-bottom:0.25em">
|
|
166
|
+
<div style="color: #8e830fff; font-size: 1em;margin-bottom:0.25em; ">
|
|
167
167
|
<strong>${event._user ? event._user.name : 'Someone'}</strong> added a
|
|
168
168
|
note
|
|
169
169
|
<temba-date
|
|
@@ -171,7 +171,7 @@ export const renderTicketAction = (
|
|
|
171
171
|
display="relative"
|
|
172
172
|
></temba-date>
|
|
173
173
|
</div>
|
|
174
|
-
|
|
174
|
+
<div style="white-space: pre-wrap;">${event.note}</div>
|
|
175
175
|
</div>`
|
|
176
176
|
: null;
|
|
177
177
|
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"created_on": "2025-09-23T20:40:27.239434+00:00",
|
|
84
84
|
"msg": {
|
|
85
85
|
"urn": "tel:+250788123123",
|
|
86
|
-
"text": "No problem
|
|
86
|
+
"text": "No problem.\nWe are all done here.",
|
|
87
87
|
"channel": {
|
|
88
88
|
"uuid": "8a81e9e0-10a0-4319-9b00-ce723cfa8303",
|
|
89
89
|
"name": "SMS Channel"
|