@propbinder/mobile-design 0.2.50 → 0.2.53

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 (221) hide show
  1. package/fesm2022/propbinder-mobile-design.mjs +26206 -0
  2. package/fesm2022/propbinder-mobile-design.mjs.map +1 -0
  3. package/index.d.ts +8193 -0
  4. package/package.json +39 -3
  5. package/ng-package.json +0 -24
  6. package/src/animations/page-transitions.ts +0 -165
  7. package/src/components/action-list-item/ds-mobile-action-list-item.ts +0 -102
  8. package/src/components/action-list-item/index.ts +0 -2
  9. package/src/components/app-icon/ds-app-icon.ts +0 -133
  10. package/src/components/app-icon/index.ts +0 -2
  11. package/src/components/attachment-preview/ds-mobile-attachment-preview.css +0 -139
  12. package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +0 -164
  13. package/src/components/attachment-preview/index.ts +0 -1
  14. package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +0 -142
  15. package/src/components/avatar-with-badge/index.ts +0 -2
  16. package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +0 -71
  17. package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +0 -121
  18. package/src/components/booking-modal/ds-mobile-booking-modal.ts +0 -598
  19. package/src/components/booking-modal/ds-mobile-booking-summary.ts +0 -161
  20. package/src/components/booking-modal/index.ts +0 -4
  21. package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +0 -266
  22. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +0 -146
  23. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +0 -156
  24. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +0 -101
  25. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +0 -169
  26. package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +0 -211
  27. package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +0 -578
  28. package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +0 -614
  29. package/src/components/bottom-sheet/index.ts +0 -8
  30. package/src/components/bottom-sheet/modal-shadow-fix.ts +0 -42
  31. package/src/components/card-inline/ds-mobile-card-inline.ts +0 -301
  32. package/src/components/card-inline/index.ts +0 -2
  33. package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +0 -118
  34. package/src/components/card-inline-banner/index.ts +0 -1
  35. package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +0 -120
  36. package/src/components/card-inline-contact/index.ts +0 -1
  37. package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +0 -141
  38. package/src/components/card-inline-file/index.ts +0 -1
  39. package/src/components/chat-modal/ds-mobile-chat-modal.css +0 -159
  40. package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +0 -105
  41. package/src/components/chat-modal/ds-mobile-chat-modal.ts +0 -918
  42. package/src/components/chat-modal/index.ts +0 -8
  43. package/src/components/comment/ds-mobile-comment.ts +0 -568
  44. package/src/components/comment/index.ts +0 -2
  45. package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +0 -182
  46. package/src/components/contact-list-item/index.ts +0 -2
  47. package/src/components/content/ds-mobile-content.ts +0 -139
  48. package/src/components/content/index.ts +0 -2
  49. package/src/components/dropdown/ds-mobile-dropdown.css +0 -199
  50. package/src/components/dropdown/ds-mobile-dropdown.ts +0 -340
  51. package/src/components/dropdown/index.ts +0 -2
  52. package/src/components/ds-mobile-tabs.css +0 -407
  53. package/src/components/ds-mobile-tabs.ts +0 -216
  54. package/src/components/empty-state/ds-mobile-empty-state.ts +0 -120
  55. package/src/components/empty-state/index.ts +0 -2
  56. package/src/components/fab/ds-mobile-fab.ts +0 -315
  57. package/src/components/fab/index.ts +0 -1
  58. package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +0 -121
  59. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +0 -189
  60. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +0 -135
  61. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +0 -656
  62. package/src/components/facility-creation-modal/index.ts +0 -9
  63. package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +0 -105
  64. package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +0 -188
  65. package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +0 -460
  66. package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +0 -134
  67. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +0 -69
  68. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +0 -379
  69. package/src/components/facility-detail-modal/index.ts +0 -2
  70. package/src/components/file-attachment/ds-mobile-file-attachment.ts +0 -164
  71. package/src/components/file-attachment/index.ts +0 -2
  72. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +0 -214
  73. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +0 -84
  74. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +0 -424
  75. package/src/components/handbook-detail-modal/index.ts +0 -3
  76. package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +0 -175
  77. package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +0 -533
  78. package/src/components/handbook-folder/index.ts +0 -4
  79. package/src/components/header-content/ds-mobile-header-content.ts +0 -222
  80. package/src/components/header-content/index.ts +0 -2
  81. package/src/components/illustration/ds-mobile-illustration.ts +0 -124
  82. package/src/components/illustration/index.ts +0 -2
  83. package/src/components/index.ts +0 -124
  84. package/src/components/inline-photo/ds-mobile-inline-photo.ts +0 -361
  85. package/src/components/inline-photo/index.ts +0 -1
  86. package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +0 -132
  87. package/src/components/inline-tabs/index.ts +0 -2
  88. package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +0 -350
  89. package/src/components/interactive-list-item-booking/index.ts +0 -1
  90. package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +0 -321
  91. package/src/components/interactive-list-item-inquiry/index.ts +0 -2
  92. package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +0 -237
  93. package/src/components/interactive-list-item-message/index.ts +0 -2
  94. package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +0 -549
  95. package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +0 -124
  96. package/src/components/interactive-list-item-post/index.ts +0 -13
  97. package/src/components/lightbox/ds-mobile-lightbox-footer.ts +0 -315
  98. package/src/components/lightbox/ds-mobile-lightbox-header.ts +0 -202
  99. package/src/components/lightbox/ds-mobile-lightbox-image.ts +0 -484
  100. package/src/components/lightbox/ds-mobile-lightbox-pdf.css +0 -377
  101. package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +0 -374
  102. package/src/components/lightbox/ds-mobile-lightbox.css +0 -587
  103. package/src/components/lightbox/ds-mobile-lightbox.service.ts +0 -296
  104. package/src/components/lightbox/ds-mobile-lightbox.ts +0 -529
  105. package/src/components/lightbox/index.ts +0 -22
  106. package/src/components/list-item/ds-mobile-list-item.ts +0 -603
  107. package/src/components/list-item/index.ts +0 -2
  108. package/src/components/list-item-static/ds-mobile-list-item-static.ts +0 -133
  109. package/src/components/list-item-static/index.ts +0 -2
  110. package/src/components/loader-overlay/ds-mobile-loader-overlay.css +0 -49
  111. package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +0 -77
  112. package/src/components/loader-overlay/index.ts +0 -1
  113. package/src/components/logo/ds-logo.ts +0 -95
  114. package/src/components/logo/index.ts +0 -2
  115. package/src/components/message-bubble/ds-mobile-message-bubble.ts +0 -633
  116. package/src/components/message-bubble/index.ts +0 -7
  117. package/src/components/message-composer/ds-mobile-message-composer.ts +0 -1146
  118. package/src/components/message-composer/index.ts +0 -7
  119. package/src/components/modal/ds-mobile-modal.css +0 -163
  120. package/src/components/modal/ds-mobile-modal.service.ts +0 -329
  121. package/src/components/modal/index.ts +0 -8
  122. package/src/components/modal-base/ds-mobile-modal-base.css +0 -378
  123. package/src/components/modal-base/ds-mobile-modal-base.ts +0 -261
  124. package/src/components/modal-base/index.ts +0 -2
  125. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +0 -112
  126. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +0 -93
  127. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +0 -442
  128. package/src/components/new-inquiry-modal/index.ts +0 -4
  129. package/src/components/offline-banner/ds-mobile-offline-banner.ts +0 -135
  130. package/src/components/offline-banner/index.ts +0 -1
  131. package/src/components/page-details/ds-mobile-page-details.css +0 -83
  132. package/src/components/page-details/ds-mobile-page-details.ts +0 -282
  133. package/src/components/page-details/index.ts +0 -2
  134. package/src/components/page-main/ds-mobile-page-main.css +0 -68
  135. package/src/components/page-main/ds-mobile-page-main.ts +0 -421
  136. package/src/components/page-main/index.ts +0 -2
  137. package/src/components/post-composer/ds-mobile-post-composer.ts +0 -140
  138. package/src/components/post-composer/index.ts +0 -2
  139. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +0 -390
  140. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +0 -108
  141. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +0 -722
  142. package/src/components/post-detail-modal/index.ts +0 -9
  143. package/src/components/property-banner/ds-mobile-property-banner.ts +0 -95
  144. package/src/components/property-banner/index.ts +0 -2
  145. package/src/components/section/ds-mobile-section.ts +0 -263
  146. package/src/components/section/index.ts +0 -2
  147. package/src/components/shared/directives/index.ts +0 -2
  148. package/src/components/shared/directives/long-press.directive.ts +0 -212
  149. package/src/components/shared/index.ts +0 -3
  150. package/src/components/shared/mobile-modal-base.ts +0 -457
  151. package/src/components/shared/mobile-page-base.ts +0 -204
  152. package/src/components/swiper/ds-mobile-swiper-with-nav.ts +0 -160
  153. package/src/components/swiper/ds-mobile-swiper.ts +0 -327
  154. package/src/components/swiper/index.ts +0 -3
  155. package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +0 -129
  156. package/src/components/system-message-banner/index.ts +0 -2
  157. package/src/components/tab-bar/ds-mobile-tab-bar.css +0 -533
  158. package/src/components/tab-bar/ds-mobile-tab-bar.ts +0 -735
  159. package/src/components/tab-bar/index.ts +0 -2
  160. package/src/components/tabs/ds-mobile-tabs.css +0 -25
  161. package/src/components/tabs/ds-mobile-tabs.ts +0 -89
  162. package/src/components/tabs/index.ts +0 -2
  163. package/src/components/text-input/ds-text-input.ts +0 -287
  164. package/src/components/text-input/index.ts +0 -2
  165. package/src/examples/booking.page.ts +0 -434
  166. package/src/examples/community.page.ts +0 -776
  167. package/src/examples/handbook.page.ts +0 -324
  168. package/src/examples/home.page.ts +0 -347
  169. package/src/examples/index.ts +0 -12
  170. package/src/examples/inquiries.example.ts +0 -273
  171. package/src/examples/inquiry-detail.example.css +0 -189
  172. package/src/examples/inquiry-detail.example.ts +0 -415
  173. package/src/examples/mobile-tabs-example.component.ts +0 -208
  174. package/src/examples/post-create.page.ts +0 -311
  175. package/src/examples/post-detail.page.ts +0 -296
  176. package/src/examples/sign-in.page.ts +0 -291
  177. package/src/examples/whitelabel-demo-modal.component.ts +0 -1094
  178. package/src/examples/whitelabel-demo-modal.service.ts +0 -77
  179. package/src/models/index.ts +0 -7
  180. package/src/models/post.model.ts +0 -41
  181. package/src/pages/community.page.ts +0 -769
  182. package/src/pages/handbook.page.ts +0 -388
  183. package/src/pages/home.page.ts +0 -303
  184. package/src/pages/index.ts +0 -11
  185. package/src/pages/inquiries.example.ts +0 -273
  186. package/src/pages/inquiry-detail.example.css +0 -189
  187. package/src/pages/inquiry-detail.example.ts +0 -415
  188. package/src/pages/mobile-tabs-example.component.ts +0 -179
  189. package/src/pages/post-create.page.ts +0 -311
  190. package/src/pages/post-detail.page.ts +0 -296
  191. package/src/pages/sign-in.page.ts +0 -291
  192. package/src/pages/whitelabel-demo-modal.component.ts +0 -1094
  193. package/src/pages/whitelabel-demo-modal.service.ts +0 -77
  194. package/src/public-api.ts +0 -6
  195. package/src/services/base-modal.service.ts +0 -101
  196. package/src/services/index.ts +0 -11
  197. package/src/services/posts.service.ts +0 -542
  198. package/src/services/tracking-permission.service.ts +0 -88
  199. package/src/services/user.service.ts +0 -60
  200. package/src/services/whitelabel.service.ts +0 -675
  201. package/tsconfig.lib.json +0 -17
  202. package/tsconfig.lib.prod.json +0 -9
  203. package/tsconfig.spec.json +0 -13
  204. /package/{src/assets → assets}/fonts/Brockmann-Bold.otf +0 -0
  205. /package/{src/assets → assets}/fonts/Brockmann-BoldItalic.otf +0 -0
  206. /package/{src/assets → assets}/fonts/Brockmann-Medium.otf +0 -0
  207. /package/{src/assets → assets}/fonts/Brockmann-MediumItalic.otf +0 -0
  208. /package/{src/assets → assets}/fonts/Brockmann-Regular.otf +0 -0
  209. /package/{src/assets → assets}/fonts/Brockmann-RegularItalic.otf +0 -0
  210. /package/{src/assets → assets}/fonts/Brockmann-SemiBold.otf +0 -0
  211. /package/{src/assets → assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
  212. /package/{src/assets → assets}/fonts/Brockmann_desktop_license.pdf +0 -0
  213. /package/{src/assets → assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
  214. /package/{src/assets → assets}/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
  215. /package/{src/assets → assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
  216. /package/{src/assets → assets}/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
  217. /package/{src/assets → assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
  218. /package/{src/assets → assets}/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
  219. /package/{src/styles → styles}/ionic.css +0 -0
  220. /package/{src/components/shared → styles}/mobile-common.css +0 -0
  221. /package/{src/components/shared → styles}/mobile-page-base.css +0 -0
@@ -1,918 +0,0 @@
1
- import { Component, signal, Input, Output, EventEmitter, OnInit, AfterViewInit, CUSTOM_ELEMENTS_SCHEMA, computed } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { Keyboard } from '@capacitor/keyboard';
4
- import { DsMobileMessageComposerComponent } from '../message-composer';
5
- import { DsMobileMessageBubbleComponent, ChatAttachment } from '../message-bubble';
6
- import { DsAvatarWithBadgeComponent } from '../avatar-with-badge';
7
- import { DsMobileModalBaseComponent } from '../modal-base/ds-mobile-modal-base';
8
- import { DsMobileCardInlineFileComponent } from '../card-inline-file';
9
- import { type AttachmentData } from '../attachment-preview';
10
- import { DsMobileLightboxService, LightboxAuthor, LightboxImage } from '../lightbox';
11
- import { DsMobileSystemMessageBannerComponent } from '../system-message-banner';
12
-
13
- import { DsIconComponent } from '@propbinder/design-system';
14
- import { DsMobileSectionComponent } from '../section';
15
-
16
- /**
17
- * Chat message data interface
18
- */
19
- export interface ChatMessage {
20
- id: string;
21
- content: string;
22
- senderId: string;
23
- senderName: string;
24
- senderRole?: string;
25
- timestamp: Date;
26
- avatarInitials?: string;
27
- avatarType?: 'initials' | 'photo' | 'icon';
28
- avatarSrc?: string;
29
- isOwnMessage: boolean;
30
- attachments?: ChatAttachment[];
31
- fileAttachments?: AttachmentData[];
32
- isNewMessage?: boolean;
33
- }
34
-
35
- /**
36
- * Message group interface for grouped timestamp display
37
- */
38
- interface MessageGroup {
39
- timestamp: Date;
40
- displayTimestamp: string;
41
- messages: ChatMessage[];
42
- }
43
-
44
- /**
45
- * Extended message interface with display metadata
46
- */
47
- interface MessageDisplay extends ChatMessage {
48
- showAvatar: boolean;
49
- clusterPosition: 'single' | 'first' | 'middle' | 'last';
50
- isNewMessage?: boolean;
51
- }
52
-
53
- /**
54
- * Chat participant interface
55
- */
56
- export interface ChatParticipant {
57
- id: string;
58
- name: string;
59
- role?: string;
60
- avatarInitials?: string;
61
- avatarType?: 'initials' | 'photo' | 'icon';
62
- avatarSrc?: string;
63
- badge?: string;
64
- verified?: boolean;
65
- lastActive?: string;
66
- }
67
-
68
- /**
69
- * Chat modal data interface
70
- *
71
- * Represents the data needed to display a chat conversation.
72
- *
73
- * @example
74
- * ```typescript
75
- * const chatData: ChatModalData = {
76
- * participant: {
77
- * id: 'u-123',
78
- * name: 'Ricki Meihlen',
79
- * role: 'Inquiry assignee',
80
- * avatarInitials: 'RM',
81
- * avatarType: 'initials'
82
- * },
83
- * currentUserId: 'u-456',
84
- * currentUserInitials: 'SD',
85
- * currentUserAvatarType: 'initials',
86
- * autoFocus: true,
87
- * messages: [
88
- * {
89
- * id: 'm-1',
90
- * content: 'We have received your case. Please see the attached photo.',
91
- * senderId: 'u-123',
92
- * senderName: 'Ricki Meihlen',
93
- * senderRole: 'Case worker',
94
- * timestamp: '12:34',
95
- * isOwnMessage: false,
96
- * avatarInitials: 'RM',
97
- * avatarType: 'initials',
98
- * attachments: [
99
- * {
100
- * id: 'a-1',
101
- * type: 'image',
102
- * url: 'https://example.com/photo.jpg',
103
- * name: 'photo.jpg',
104
- * thumbnail: 'https://example.com/photo_thumb.jpg'
105
- * },
106
- * {
107
- * id: 'a-2',
108
- * type: 'pdf',
109
- * url: 'https://example.com/report.pdf',
110
- * name: 'report.pdf'
111
- * }
112
- * ]
113
- * },
114
- * {
115
- * id: 'm-2',
116
- * content: 'Thanks — I will take a look.',
117
- * senderId: 'u-456',
118
- * senderName: 'You',
119
- * timestamp: '12:36',
120
- * isOwnMessage: true,
121
- * avatarInitials: 'SD',
122
- * avatarType: 'initials'
123
- * }
124
- * ]
125
- * };
126
- * ```
127
- */
128
- export interface ChatModalData {
129
- /** The other participant in the conversation */
130
- participant: ChatParticipant;
131
- /** Array of messages in the conversation */
132
- messages: ChatMessage[];
133
- /** Current user's ID */
134
- currentUserId: string;
135
- /** Current user's avatar initials */
136
- currentUserInitials?: string;
137
- /** Current user's avatar type */
138
- currentUserAvatarType?: 'initials' | 'photo' | 'icon';
139
- /** Current user's avatar source */
140
- currentUserAvatarSrc?: string;
141
- /** Auto-focus input when modal opens */
142
- autoFocus?: boolean;
143
- /**
144
- * Callback executed when a message is sent from the chat.
145
- * Use this to handle API calls to your backend.
146
- */
147
- onSend?: (message: string, attachments: AttachmentData[]) => void | Promise<void>;
148
- /**
149
- * Optional callback for when a file attachment is clicked.
150
- * If not provided, the default behavior will try to open/download the file.
151
- */
152
- onFileClick?: (file: AttachmentData) => void;
153
- }
154
-
155
- /**
156
- * DsMobileChatModalComponent
157
- *
158
- * Modal component for displaying and managing chat conversations.
159
- * Follows the same pattern as post-detail-modal for consistent behavior.
160
- *
161
- * Features:
162
- * - Header with participant info and close button
163
- * - Scrollable message thread
164
- * - Fixed message composer at bottom
165
- * - Keyboard handling
166
- * - Safe area support
167
- *
168
- * This component is typically not used directly - use DsMobileChatModalService instead.
169
- *
170
- * @example
171
- * ```typescript
172
- * // Don't instantiate directly - use the service:
173
- * constructor(private chatModal: DsMobileChatModalService) {}
174
- *
175
- * openChat() {
176
- * this.chatModal.open({
177
- * participant: { id: '123', name: 'Ricki Meihlen' },
178
- * messages: [...],
179
- * currentUserId: '456'
180
- * });
181
- * }
182
- * ```
183
- */
184
- @Component({
185
- selector: 'ds-mobile-chat-modal',
186
- standalone: true,
187
- imports: [
188
- CommonModule,
189
- DsAvatarWithBadgeComponent,
190
- DsMobileMessageComposerComponent,
191
- DsMobileMessageBubbleComponent,
192
- DsMobileModalBaseComponent,
193
- DsMobileCardInlineFileComponent,
194
- DsMobileSystemMessageBannerComponent,
195
- DsIconComponent,
196
- DsMobileSectionComponent,
197
- ],
198
- styleUrls: ['../shared/mobile-common.css', './ds-mobile-chat-modal.css'],
199
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
200
- template: `
201
- <ds-mobile-modal-base
202
- [loading]="loading"
203
- [error]="error"
204
- [headerTitle]="participant().name"
205
- [headerMeta]="participant().role || ''"
206
- [hasFixedBottom]="true"
207
- [isAutoHeight]="false"
208
- [enableKeyboardHandling]="true"
209
- (keyboardWillShow)="handleKeyboardShow($event)"
210
- closeButtonLabel="Luk chat"
211
- >
212
- <!-- Header Avatar -->
213
- <ds-avatar-with-badge
214
- header-leading
215
- [initials]="participant().avatarInitials || ''"
216
- [type]="participant().avatarType || 'initials'"
217
- [src]="participant().avatarSrc || ''"
218
- size="md"
219
- />
220
-
221
- <!-- Messages Section -->
222
- <ds-mobile-section>
223
- <div class="chat-messages-container" (click)="handleContentClick()">
224
- <!-- Centered avatar section at top of messages -->
225
- <div class="chat-avatar-section">
226
- <ds-avatar-with-badge
227
- [initials]="participant().avatarInitials || ''"
228
- [type]="participant().avatarType || 'initials'"
229
- [src]="participant().avatarSrc || ''"
230
- [badge]="participant().badge"
231
- size="xl"
232
- />
233
- <div class="chat-avatar-info">
234
- <div class="chat-avatar-name">
235
- {{ participant().name }}
236
- @if (participant().verified) {
237
- <ds-icon name="remixCheckboxCircleFill" size="24px" [style.color]="'var(--color-primary-base)'"></ds-icon>
238
- }
239
- </div>
240
- @if (participant().role) {
241
- <div class="chat-avatar-role">{{ participant().role }}</div>
242
- }
243
- @if (participant().lastActive) {
244
- <div class="chat-avatar-meta">{{ participant().lastActive }}</div>
245
- }
246
- </div>
247
- </div>
248
-
249
- <div class="messages-list">
250
- @if (messages().length === 0) {
251
- <!-- Empty State - Timestamp and System Message -->
252
- <div class="timestamp-header">
253
- <span class="timestamp-text">{{ getInitialTimestamp() }}</span>
254
- </div>
255
-
256
- <ds-mobile-system-message-banner [message]="participant().name + ' har overtaget din henvendelse og vil kontakte dig snart.'" [afterTimestamp]="true">
257
- </ds-mobile-system-message-banner>
258
- } @else {
259
- @for (group of messagesWithDisplay(); track group.timestamp) {
260
- <!-- Timestamp Header -->
261
- <div class="timestamp-header">
262
- <span class="timestamp-text">{{ group.displayTimestamp }}</span>
263
- </div>
264
-
265
- <!-- System message example (shown after first timestamp) -->
266
- @if ($first) {
267
- <ds-mobile-system-message-banner [message]="participant().name + ' har overtaget din henvendelse og vil kontakte dig snart.'" [afterTimestamp]="true">
268
- </ds-mobile-system-message-banner>
269
- }
270
- @for (message of group.messages; track message.id) {
271
- <!-- Only show bubble if has content -->
272
- @if (message.content.trim()) {
273
- <ds-mobile-message-bubble
274
- [content]="message.content"
275
- [isOwnMessage]="message.isOwnMessage"
276
- [timestamp]="formatMessageTimestamp(message.timestamp)"
277
- [showTimestamp]="selectedMessageId() === message.id"
278
- [avatarInitials]="message.avatarInitials || ''"
279
- [avatarType]="message.avatarType || 'initials'"
280
- [avatarSrc]="message.avatarSrc || ''"
281
- [showAvatar]="message.showAvatar"
282
- [clusterPosition]="message.clusterPosition"
283
- [attachments]="message.attachments"
284
- [clickable]="true"
285
- [isNewMessage]="message.isNewMessage || false"
286
- (messageClick)="handleMessageClick(message.id)"
287
- (attachmentClick)="handleAttachmentClick($event)"
288
- (longPress)="handleMessageLongPress(message)"
289
- >
290
- </ds-mobile-message-bubble>
291
- }
292
-
293
- <!-- File attachments displayed below message bubble -->
294
- @if (message.fileAttachments && message.fileAttachments.length > 0) {
295
- <div class="message-file-attachments" [class.own-message]="message.isOwnMessage">
296
- @for (fileAttachment of message.fileAttachments; track fileAttachment.id) {
297
- <!-- Show inline image preview for image attachments -->
298
- @if (fileAttachment.type === 'image') {
299
- <div class="message-image-attachment" (click)="handleImageClick(fileAttachment, message)">
300
- <img [src]="fileAttachment.src" [alt]="fileAttachment.name || 'Image'" class="inline-image" />
301
- </div>
302
- } @else {
303
- <!-- Show file card for non-image attachments -->
304
- <ds-mobile-card-inline-file
305
- [fileName]="fileAttachment.name || 'Unknown file'"
306
- [fileSize]="fileAttachment.size || ''"
307
- [variant]="getFileVariant(fileAttachment.type)"
308
- [layout]="'compact'"
309
- (fileClick)="handleFileAttachmentClick(fileAttachment)"
310
- >
311
- </ds-mobile-card-inline-file>
312
- }
313
- }
314
- </div>
315
- }
316
- }
317
- }
318
- }
319
- </div>
320
- </div>
321
- </ds-mobile-section>
322
-
323
- <!-- Fixed message composer -->
324
- <div fixed-bottom>
325
- <ds-mobile-message-composer
326
- [avatarInitials]="currentUserInitials()"
327
- [avatarType]="currentUserAvatarType()"
328
- [avatarSrc]="currentUserAvatarSrc()"
329
- [placeholder]="'Skriv en besked...'"
330
- [autoFocus]="autoFocus()"
331
- [showAttachmentButton]="true"
332
- (messageSent)="handleMessageSent($event)"
333
- (attachmentClicked)="handleComposerAttachmentClick()"
334
- (attachmentsChanged)="handleAttachmentsChanged()"
335
- >
336
- </ds-mobile-message-composer>
337
- </div>
338
- </ds-mobile-modal-base>
339
- `,
340
- })
341
- export class DsMobileChatModalComponent implements OnInit, AfterViewInit {
342
- // Chat data passed from service
343
- @Input() chatData!: ChatModalData;
344
-
345
- /**
346
- * Loading state - when true, shows loading indicator
347
- */
348
- @Input() loading: boolean = false;
349
-
350
- /**
351
- * Error state - when set, shows error message
352
- */
353
- @Input() error?: string;
354
-
355
- /**
356
- * Back button click event
357
- */
358
- @Output() back = new EventEmitter<void>();
359
-
360
- // Signal for reactive chat data
361
- participant = signal<ChatParticipant>({
362
- id: '',
363
- name: '',
364
- avatarInitials: '',
365
- });
366
-
367
- messages = signal<ChatMessage[]>([]);
368
- currentUserInitials = signal<string>('');
369
- currentUserAvatarType = signal<'initials' | 'photo' | 'icon'>('initials');
370
- currentUserAvatarSrc = signal<string>('');
371
- autoFocus = signal<boolean>(false);
372
-
373
- /**
374
- * Selected message ID for showing timestamp
375
- */
376
- selectedMessageId = signal<string | null>(null);
377
-
378
- /**
379
- * Track if keyboard is currently visible
380
- */
381
- private isKeyboardVisible = false;
382
-
383
- /**
384
- * Timeout for auto-hiding timestamp
385
- */
386
- private timestampTimeout?: ReturnType<typeof setTimeout>;
387
-
388
- /**
389
- * Computed signal for grouped messages with timestamp headers
390
- */
391
- messageGroups = computed(() => {
392
- return this.groupMessagesByTime(this.messages(), 5);
393
- });
394
-
395
- /**
396
- * Computed signal for messages with display metadata (avatar visibility)
397
- */
398
- messagesWithDisplay = computed(() => {
399
- return this.addDisplayMetadata(this.messageGroups());
400
- });
401
-
402
- constructor(private lightboxService: DsMobileLightboxService) {}
403
-
404
- ngOnInit(): void {
405
- // Initialize chat data from input
406
- if (this.chatData) {
407
- this.participant.set(this.chatData.participant);
408
- this.messages.set(this.chatData.messages || []);
409
- this.currentUserInitials.set(this.chatData.currentUserInitials || '');
410
- this.currentUserAvatarType.set(this.chatData.currentUserAvatarType || 'initials');
411
- this.currentUserAvatarSrc.set(this.chatData.currentUserAvatarSrc || '');
412
- this.autoFocus.set(this.chatData.autoFocus || false);
413
- }
414
- }
415
-
416
- ngAfterViewInit(): void {
417
- // Scroll to bottom when messages load
418
- if (this.messages().length > 0) {
419
- setTimeout(() => {
420
- this.scrollToBottom();
421
- }, 500);
422
- }
423
- }
424
-
425
- /**
426
- * Handle back button click
427
- */
428
- handleBack(): void {
429
- this.back.emit();
430
- }
431
-
432
- /**
433
- * Handle keyboard show event from base modal
434
- * Only scrolls to bottom if user is already near the bottom, otherwise lets
435
- * the natural padding expansion push content up (like Messenger/Signal/Telegram)
436
- */
437
- handleKeyboardShow(keyboardHeight: number): void {
438
- // Track keyboard state
439
- this.isKeyboardVisible = true;
440
-
441
- // Check if user is near bottom before auto-scrolling
442
- this.isNearBottom().then((isNear) => {
443
- if (isNear) {
444
- // User is already viewing latest messages, maintain that position
445
- // Small delay to synchronize with keyboard animation
446
- setTimeout(() => {
447
- this.scrollToBottom();
448
- }, 50);
449
- }
450
- // Otherwise, let the natural padding expansion push content up smoothly
451
- });
452
- }
453
-
454
- /**
455
- * Check if scroll position is near the bottom
456
- * Used to determine if we should auto-scroll when keyboard appears
457
- */
458
- private async isNearBottom(): Promise<boolean> {
459
- const ionContent = document.querySelector('ds-mobile-chat-modal ion-content');
460
- if (ionContent) {
461
- try {
462
- const scrollElement = await (ionContent as any).getScrollElement();
463
- const scrollTop = scrollElement.scrollTop;
464
- const scrollHeight = scrollElement.scrollHeight;
465
- const clientHeight = scrollElement.clientHeight;
466
- const threshold = 150; // pixels - consider "near bottom" within 150px
467
-
468
- const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
469
- return distanceFromBottom <= threshold;
470
- } catch (e) {
471
- console.log('[ChatModal] Could not check scroll position:', e);
472
- // The provided snippet was syntactically incorrect for this location.
473
- // Assuming the intent was to add `auto-height` to the modal's CSS class,
474
- // this change should be applied where the modal is opened or in its template.
475
- // As per the instruction, the `isAutoHeight` property is added to the component.
476
- }
477
- }
478
- return true;
479
- }
480
-
481
- /**
482
- * Scroll to bottom of messages
483
- */
484
- private async scrollToBottom(): Promise<void> {
485
- const ionContent = document.querySelector('ds-mobile-chat-modal ion-content');
486
- if (ionContent) {
487
- // Scroll with smooth animation (400ms) and add a small offset to ensure visibility
488
- await (ionContent as any).scrollToBottom(400);
489
- }
490
- }
491
-
492
- /**
493
- * Handle message sent from composer
494
- */
495
- handleMessageSent(event: { content: string; isReply?: boolean; replyTo?: string; isEdit?: boolean; attachments?: AttachmentData[] }): void {
496
- const newMessage: ChatMessage = {
497
- id: Date.now().toString(),
498
- content: event.content,
499
- senderId: this.chatData.currentUserId,
500
- senderName: 'You',
501
- timestamp: new Date(),
502
- isOwnMessage: true,
503
- avatarInitials: this.currentUserInitials(),
504
- avatarType: this.currentUserAvatarType(),
505
- avatarSrc: this.currentUserAvatarSrc(),
506
- fileAttachments: event.attachments || [],
507
- isNewMessage: true, // Mark as new message to trigger animation
508
- };
509
-
510
- // Add message to list
511
- const updatedMessages = [...this.messages(), newMessage];
512
- this.messages.set(updatedMessages);
513
-
514
- // Remove the isNewMessage flag after animation completes (using spring-bouncy duration ~600ms)
515
- setTimeout(() => {
516
- const msgs = this.messages();
517
- const msgIndex = msgs.findIndex((m) => m.id === newMessage.id);
518
- if (msgIndex !== -1) {
519
- msgs[msgIndex].isNewMessage = false;
520
- this.messages.set([...msgs]);
521
- }
522
- }, 700);
523
-
524
- // Scroll to bottom after message is added and DOM updates
525
- // Multiple delays to ensure:
526
- // 1. DOM update from new message
527
- // 2. Composer height change (clears text/attachments)
528
- // 3. ResizeObserver updates padding
529
- requestAnimationFrame(() => {
530
- requestAnimationFrame(() => {
531
- setTimeout(() => {
532
- this.scrollToBottom();
533
- console.log('[ChatModal] Scrolled to bottom after message sent');
534
- }, 150);
535
- });
536
- });
537
-
538
- // Executing the onSend callback if provided
539
- if (this.chatData.onSend) {
540
- this.chatData.onSend(event.content, event.attachments || []);
541
- }
542
- }
543
-
544
- /**
545
- * Handle attachment click
546
- */
547
- handleAttachmentClick(attachment: ChatAttachment): void {
548
- if (attachment.type !== 'image') {
549
- if (attachment.url) {
550
- window.open(attachment.url, '_blank', 'noopener,noreferrer');
551
- }
552
- return;
553
- }
554
-
555
- const msgs = this.messages();
556
-
557
- const allImages: Array<{ msg: ChatMessage; att: ChatAttachment }> = [];
558
- for (const m of msgs) {
559
- for (const att of m.attachments ?? []) {
560
- if (att.type === 'image') {
561
- allImages.push({ msg: m, att });
562
- }
563
- }
564
- }
565
-
566
- const initialIndex = Math.max(
567
- 0,
568
- allImages.findIndex((x) => x.att.id === attachment.id),
569
- );
570
-
571
- const images: LightboxImage[] = allImages.map((x) => ({
572
- type: 'image',
573
- src: x.att.url,
574
- title: x.att.name,
575
- alt: x.att.name,
576
- thumbnail: x.att.thumbnail,
577
- }));
578
-
579
- const ownerMessage = allImages[initialIndex]?.msg;
580
-
581
- const author: LightboxAuthor | undefined = ownerMessage
582
- ? {
583
- name: ownerMessage.senderName,
584
- role: ownerMessage.senderRole,
585
- timestamp: this.formatMessageTimestamp(ownerMessage.timestamp),
586
- avatarInitials: ownerMessage.avatarInitials,
587
- avatarSrc: ownerMessage.avatarSrc,
588
- avatarType: ownerMessage.avatarType === 'photo' || ownerMessage.avatarType === 'initials' ? ownerMessage.avatarType : undefined,
589
- }
590
- : undefined;
591
-
592
- this.lightboxService.openImages({
593
- images,
594
- author,
595
- initialIndex,
596
- enableZoom: true,
597
- enableSwipe: true,
598
- showControls: true,
599
- showInfo: true,
600
- animation: 'fade',
601
- });
602
- }
603
-
604
- /**
605
- * Handle composer attachment button click
606
- */
607
- handleComposerAttachmentClick(): void {
608
- console.log('[ChatModal] Composer attachment button clicked');
609
- // In a real app, you would open a file picker or show attachment options
610
- }
611
-
612
- /**
613
- * Handle attachments changed (added or removed)
614
- * ResizeObserver automatically updates padding, which naturally pushes content up.
615
- * Only scroll to bottom if user is already viewing the latest messages.
616
- */
617
- handleAttachmentsChanged(): void {
618
- console.log('[ChatModal] Attachments changed - ResizeObserver will handle padding naturally');
619
- // ResizeObserver automatically:
620
- // 1. Detects composer height change
621
- // 2. Updates --fixed-bottom-height CSS variable
622
- // 3. Updates scroll element padding
623
- // 4. Smoothly pushes content up
624
-
625
- // Only scroll if user is already at bottom (optional - can be removed entirely)
626
- requestAnimationFrame(() => {
627
- requestAnimationFrame(() => {
628
- setTimeout(() => {
629
- this.isNearBottom().then((isNear) => {
630
- if (isNear) {
631
- // User is at bottom, maintain that position as composer expands
632
- this.scrollToBottom();
633
- }
634
- // Otherwise, let natural padding expansion push content up
635
- });
636
- }, 100);
637
- });
638
- });
639
- }
640
-
641
- /**
642
- * Handle content area click - dismiss keyboard when tapping messages
643
- */
644
- handleContentClick(): void {
645
- // Hide keyboard when tapping outside the composer
646
- Keyboard.hide()
647
- .then(() => {
648
- this.isKeyboardVisible = false;
649
- })
650
- .catch((e) => console.log('[ChatModal] Keyboard.hide() not available:', e));
651
- }
652
-
653
- /**
654
- * Get file variant for card-inline-file component
655
- */
656
- getFileVariant(type: string): 'pdf' | 'doc' {
657
- return type === 'pdf' ? 'pdf' : 'doc';
658
- }
659
-
660
- /**
661
- * Handle file attachment click
662
- */
663
- handleFileAttachmentClick(fileAttachment: AttachmentData): void {
664
- // If a custom handler is provided, use it
665
- if (this.chatData.onFileClick) {
666
- this.chatData.onFileClick(fileAttachment);
667
- return;
668
- }
669
-
670
- // Default behavior: Try to open/download the file
671
- const url = fileAttachment.src || (fileAttachment as any).url;
672
- if (url) {
673
- const link = document.createElement('a');
674
- link.href = url;
675
- link.target = '_blank';
676
- // If it has a name, setting download attribute suggests downloading
677
- if (fileAttachment.name) {
678
- link.download = fileAttachment.name;
679
- }
680
-
681
- document.body.appendChild(link);
682
- link.click();
683
- document.body.removeChild(link);
684
- } else {
685
- console.warn('[ChatModal] No URL or source for file attachment:', fileAttachment);
686
- }
687
- }
688
-
689
- /**
690
- * Handle image attachment click - opens lightbox
691
- */
692
- async handleImageClick(attachment: AttachmentData, message: ChatMessage): Promise<void> {
693
- await this.lightboxService.openImages({
694
- images: [
695
- {
696
- type: 'image',
697
- src: attachment.src,
698
- title: attachment.name || 'Image',
699
- alt: attachment.name || 'Chat image',
700
- },
701
- ],
702
- author: {
703
- name: message.senderName,
704
- role: message.senderRole,
705
- avatarInitials: message.avatarInitials,
706
- avatarType: message.avatarType === 'initials' ? 'initials' : 'photo',
707
- avatarSrc: message.avatarSrc,
708
- timestamp: this.formatMessageTimestamp(message.timestamp),
709
- },
710
- initialIndex: 0,
711
- enableZoom: true,
712
- showControls: false,
713
- enableSwipe: false,
714
- showInfo: false,
715
- showActions: false,
716
- });
717
- }
718
-
719
- /**
720
- * Handle message long press
721
- */
722
- handleMessageLongPress(message: ChatMessage): void {
723
- console.log('[ChatModal] Message long pressed:', message);
724
- // In a real app, you would show an action sheet with options (copy, delete, etc.)
725
- }
726
-
727
- /**
728
- * Handle message click to show/hide timestamp
729
- * Only shows timestamp if keyboard is already hidden
730
- */
731
- handleMessageClick(messageId: string): void {
732
- // If keyboard is visible, dismiss it and don't show timestamp
733
- if (this.isKeyboardVisible) {
734
- Keyboard.hide()
735
- .then(() => {
736
- this.isKeyboardVisible = false;
737
- })
738
- .catch((e) => console.log('[ChatModal] Keyboard.hide() not available:', e));
739
- return; // Exit early, don't toggle timestamp
740
- }
741
-
742
- // Keyboard is hidden, proceed with timestamp toggle
743
- // Clear existing timeout
744
- if (this.timestampTimeout) {
745
- clearTimeout(this.timestampTimeout);
746
- }
747
-
748
- // Toggle timestamp - if clicking same message, hide it; otherwise show new one
749
- this.selectedMessageId.update((current) => (current === messageId ? null : messageId));
750
-
751
- // Auto-hide after 3 seconds if showing
752
- if (this.selectedMessageId() === messageId) {
753
- this.timestampTimeout = setTimeout(() => {
754
- this.selectedMessageId.set(null);
755
- }, 3000);
756
- }
757
- }
758
-
759
- /**
760
- * Format message timestamp for display (EU 24-hour format, Danish)
761
- */
762
- formatMessageTimestamp(date: Date): string {
763
- return date.toLocaleTimeString('da-DK', {
764
- hour: '2-digit',
765
- minute: '2-digit',
766
- hour12: false,
767
- });
768
- }
769
-
770
- /**
771
- * Get initial timestamp for empty chat state
772
- * Returns current time formatted with smart date display
773
- */
774
- getInitialTimestamp(): string {
775
- return this.formatGroupTimestamp(new Date());
776
- }
777
-
778
- /**
779
- * Group messages by time threshold
780
- * Messages within the threshold and on the same day are grouped together
781
- */
782
- private groupMessagesByTime(messages: ChatMessage[], thresholdMinutes: number): MessageGroup[] {
783
- const groups: MessageGroup[] = [];
784
- let currentGroup: MessageGroup | null = null;
785
-
786
- messages.forEach((message) => {
787
- const messageDate = message.timestamp;
788
-
789
- // Start a new group if:
790
- // 1. It's the first message
791
- // 2. More than threshold minutes have passed since last message
792
- // 3. Date changed (new day)
793
- if (!currentGroup || this.shouldStartNewGroup(currentGroup.timestamp, messageDate, thresholdMinutes)) {
794
- currentGroup = {
795
- timestamp: messageDate,
796
- displayTimestamp: this.formatGroupTimestamp(messageDate),
797
- messages: [],
798
- };
799
- groups.push(currentGroup);
800
- }
801
-
802
- currentGroup.messages.push(message);
803
- });
804
-
805
- return groups;
806
- }
807
-
808
- /**
809
- * Determine if a new message group should be started
810
- */
811
- private shouldStartNewGroup(lastTime: Date, currentTime: Date, thresholdMinutes: number): boolean {
812
- const diffMinutes = (currentTime.getTime() - lastTime.getTime()) / (1000 * 60);
813
-
814
- // New group if different day
815
- if (lastTime.toDateString() !== currentTime.toDateString()) {
816
- return true;
817
- }
818
-
819
- // New group if threshold exceeded
820
- return diffMinutes > thresholdMinutes;
821
- }
822
-
823
- /**
824
- * Format group timestamp header with smart date display
825
- * Uses 24-hour EU time format with Danish locale
826
- */
827
- private formatGroupTimestamp(date: Date): string {
828
- const now = new Date();
829
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
830
- const yesterday = new Date(today);
831
- yesterday.setDate(yesterday.getDate() - 1);
832
-
833
- const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
834
-
835
- // Format time in 24-hour EU format
836
- const timeStr = date.toLocaleTimeString('da-DK', {
837
- hour: '2-digit',
838
- minute: '2-digit',
839
- hour12: false,
840
- });
841
-
842
- // Today: "14:34"
843
- if (messageDate.getTime() === today.getTime()) {
844
- return timeStr;
845
- }
846
-
847
- // Yesterday: "I går, 14:34"
848
- if (messageDate.getTime() === yesterday.getTime()) {
849
- return `I går, ${timeStr}`;
850
- }
851
-
852
- // This week: "Mandag, 14:34"
853
- const daysAgo = Math.floor((today.getTime() - messageDate.getTime()) / (1000 * 60 * 60 * 24));
854
- if (daysAgo < 7) {
855
- return date.toLocaleDateString('da-DK', { weekday: 'long' }) + `, ${timeStr}`;
856
- }
857
-
858
- // Older: "15. jan, 14:34" or "20. dec. 2024, 14:34" if different year
859
- const dateFormat: Intl.DateTimeFormatOptions = {
860
- month: 'short',
861
- day: 'numeric',
862
- };
863
-
864
- if (date.getFullYear() !== now.getFullYear()) {
865
- dateFormat.year = 'numeric';
866
- }
867
-
868
- return date.toLocaleDateString('da-DK', dateFormat) + `, ${timeStr}`;
869
- }
870
-
871
- /**
872
- * Add display metadata to message groups
873
- * Determines which messages should show avatars (only last in cluster)
874
- * and calculates cluster positions for border radius styling
875
- */
876
- private addDisplayMetadata(groups: MessageGroup[]): Array<MessageGroup & { messages: MessageDisplay[] }> {
877
- return groups.map((group) => {
878
- const messagesWithDisplay: MessageDisplay[] = [];
879
-
880
- for (let i = 0; i < group.messages.length; i++) {
881
- const currentMsg = group.messages[i];
882
- const prevMsg = group.messages[i - 1];
883
- const nextMsg = group.messages[i + 1];
884
-
885
- // Determine if this message starts a new cluster
886
- const startsCluster = !prevMsg || prevMsg.senderId !== currentMsg.senderId;
887
- // Determine if this message ends the cluster
888
- const endsCluster = !nextMsg || nextMsg.senderId !== currentMsg.senderId;
889
-
890
- // Show avatar only at the end of a cluster
891
- const showAvatar = endsCluster;
892
-
893
- // Determine cluster position for border radius
894
- let clusterPosition: 'single' | 'first' | 'middle' | 'last';
895
- if (startsCluster && endsCluster) {
896
- clusterPosition = 'single';
897
- } else if (startsCluster) {
898
- clusterPosition = 'first';
899
- } else if (endsCluster) {
900
- clusterPosition = 'last';
901
- } else {
902
- clusterPosition = 'middle';
903
- }
904
-
905
- messagesWithDisplay.push({
906
- ...currentMsg,
907
- showAvatar,
908
- clusterPosition,
909
- });
910
- }
911
-
912
- return {
913
- ...group,
914
- messages: messagesWithDisplay,
915
- };
916
- });
917
- }
918
- }