@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.
@@ -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: string;
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: string[][] = [];
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(msg1: ContactEvent, msg2: ContactEvent): boolean {
717
- if (msg1 && msg2) {
718
- const sameGroup =
719
- msg1.type === msg2.type &&
720
- msg1._user?.name === msg2._user?.name &&
721
- Math.abs(msg1.created_on.getTime() - msg2.created_on.getTime()) <
722
- BATCH_TIME_WINDOW;
723
- return sameGroup;
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 false;
777
+ return { same: true };
727
778
  }
728
779
 
729
- private insertGroups(newGroups: string[][], append = false) {
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
- if (this.isSameGroup(lastMsg, newMsg)) {
745
- group.push(...newGroup);
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[]): 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
- if (!this.isSameGroup(msg, lastMsg)) {
774
- lastGroup = [];
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
- lastGroup.push(msgId);
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
- msgIds: string[],
904
+ group: MessageGroup,
827
905
  idx: number,
828
- groups: string[][]
829
- ): TemplateResult {
906
+ lastShownTimestamp: Date | null
907
+ ): { html: TemplateResult; timestamp: Date | null } {
830
908
  const today = new Date();
831
- const firstGroup = idx === groups.length - 1;
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
- return html`
882
- ${timeDisplay}
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 message-text">${message.msg.text}</div>
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
- ? repeat(
1010
- this.messageGroups,
1011
- (msgGroup) => msgGroup.join(','),
1012
- (msgGroup, idx) =>
1013
- html`${this.renderMessageGroup(
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
- this.messageGroups
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
@@ -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 0; border: 1px solid #ffe97f;border-radius: var(--curvature);"
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
- ${event.note}
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, we are all done then!",
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"