@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,1146 +0,0 @@
1
- import { Component, input, output, signal, computed, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { FormsModule } from '@angular/forms';
4
- import { Capacitor } from '@capacitor/core';
5
- import { Keyboard } from '@capacitor/keyboard';
6
- import { FilePicker } from '@capawesome/capacitor-file-picker';
7
- import { DsAvatarComponent } from '@propbinder/design-system';
8
- import { DsIconButtonComponent } from '@propbinder/design-system';
9
- import { DsIconComponent } from '@propbinder/design-system';
10
- import { DsMobileAttachmentPreviewComponent, type AttachmentData, type AttachmentFileType } from '../attachment-preview/ds-mobile-attachment-preview';
11
- import { DsMobileDropdownComponent, type DsMobileDropdownItem } from '../dropdown';
12
-
13
- /**
14
- * DsMobileMessageComposerComponent
15
- *
16
- * Reusable message composer component extracted from post-detail-modal.
17
- * Can be used for both comments and chat messages.
18
- *
19
- * Features:
20
- * - Text input with auto-resize
21
- * - Avatar display
22
- * - Send button (appears when text is entered)
23
- * - Optional mention menu support
24
- * - Optional edit/reply indicators
25
- * - Keyboard handling for mobile
26
- * - Safe area support
27
- *
28
- * @example
29
- * ```html
30
- * <ds-mobile-message-composer
31
- * [avatarInitials]="'JD'"
32
- * [placeholder]="'Write a message...'"
33
- * (messageSent)="handleMessage($event)">
34
- * </ds-mobile-message-composer>
35
- * ```
36
- */
37
- @Component({
38
- selector: 'ds-mobile-message-composer',
39
- standalone: true,
40
- imports: [CommonModule, FormsModule, DsAvatarComponent, DsIconButtonComponent, DsIconComponent, DsMobileAttachmentPreviewComponent, DsMobileDropdownComponent],
41
- styles: [
42
- `
43
- :host {
44
- display: block;
45
- }
46
-
47
- /* Composer Container */
48
- .message-composer {
49
- background: var(--color-background-neutral-primary, #ffffff);
50
- border-top: 1px solid var(--border-color-default);
51
- border-bottom-left-radius: 0;
52
- border-bottom-right-radius: 0;
53
- padding: 12px 16px;
54
- width: 100%;
55
- display: flex;
56
- flex-direction: column;
57
- gap: 8px;
58
- }
59
-
60
- /* Edit indicator (optional) */
61
- .edit-indicator {
62
- display: flex;
63
- align-items: center;
64
- justify-content: space-between;
65
- padding: 8px 12px;
66
- background: var(--color-background-brand-subtle, #f0edfe);
67
- border-radius: 8px;
68
- animation: slideDown 0.2s ease-out;
69
- }
70
-
71
- .edit-indicator-content {
72
- display: flex;
73
- align-items: center;
74
- gap: 8px;
75
- color: var(--color-accent, #6b5ff5);
76
- flex: 1;
77
- min-width: 0;
78
- }
79
-
80
- .edit-text {
81
- font-family: 'Brockmann', sans-serif;
82
- font-size: var(--font-size-sm);
83
- font-weight: 500;
84
- line-height: 18px;
85
- color: var(--color-accent, #6b5ff5);
86
- }
87
-
88
- .cancel-edit {
89
- background: none;
90
- border: none;
91
- padding: 4px;
92
- cursor: pointer;
93
- display: flex;
94
- align-items: center;
95
- justify-content: center;
96
- color: var(--color-accent, #6b5ff5);
97
- border-radius: 4px;
98
- transition: background 0.2s ease;
99
- flex-shrink: 0;
100
- }
101
-
102
- .cancel-edit:active {
103
- background: var(--color-brand-subtle, #e0dbfe);
104
- }
105
-
106
- /* Reply indicator (optional) */
107
- .reply-indicator {
108
- display: flex;
109
- align-items: center;
110
- justify-content: space-between;
111
- padding: 8px 12px;
112
- background: var(--color-background-neutral-secondary, #f5f5f5);
113
- border-radius: 8px;
114
- animation: slideDown 0.2s ease-out;
115
- }
116
-
117
- .reply-indicator-content {
118
- display: flex;
119
- align-items: center;
120
- gap: 4px;
121
- color: var(--color-text-secondary, #737373);
122
- flex: 1;
123
- min-width: 0;
124
- }
125
-
126
- .reply-to-text {
127
- font-family: 'Brockmann', sans-serif;
128
- font-size: var(--font-size-sm);
129
- line-height: 18px;
130
- color: var(--color-text-secondary, #737373);
131
- white-space: nowrap;
132
- overflow: hidden;
133
- text-overflow: ellipsis;
134
- }
135
-
136
- .reply-author {
137
- color: var(--color-accent, #6b5ff5);
138
- font-weight: 600;
139
- }
140
-
141
- .cancel-reply {
142
- background: none;
143
- border: none;
144
- padding: 4px;
145
- cursor: pointer;
146
- display: flex;
147
- align-items: center;
148
- justify-content: center;
149
- color: var(--color-text-secondary, #737373);
150
- border-radius: 4px;
151
- transition: background 0.2s ease;
152
- flex-shrink: 0;
153
- }
154
-
155
- .cancel-reply:active {
156
- background: var(--color-background-neutral-secondary, #f5f5f5);
157
- }
158
-
159
- @keyframes slideDown {
160
- from {
161
- opacity: 0;
162
- transform: translateY(-10px);
163
- }
164
- to {
165
- opacity: 1;
166
- transform: translateY(0);
167
- }
168
- }
169
-
170
- /* Attachment previews section */
171
- .attachment-previews-section {
172
- padding: 0 0 8px 0;
173
- animation: slideDown 0.2s ease-out;
174
- }
175
-
176
- .attachment-previews {
177
- display: flex;
178
- flex-wrap: wrap;
179
- gap: 8px;
180
- }
181
-
182
- .composer-content {
183
- display: flex;
184
- align-items: center;
185
- gap: 12px;
186
- width: 100%;
187
- position: relative;
188
- }
189
-
190
- /* Attachment button replacing avatar */
191
- .composer-leading-button {
192
- flex-shrink: 0;
193
- }
194
-
195
- .composer-leading-button::ng-deep button {
196
- width: 40px !important;
197
- height: 40px !important;
198
- min-width: 40px !important;
199
- min-height: 40px !important;
200
- padding: 0 !important;
201
- border-radius: 50% !important;
202
- transition: transform 0.3s ease;
203
- }
204
-
205
- .composer-leading-button--open::ng-deep button {
206
- transform: rotate(45deg);
207
- }
208
-
209
- .composer-input-wrapper {
210
- flex: 1;
211
- display: flex;
212
- align-items: flex-start;
213
- gap: 8px;
214
- background: var(--color-background-neutral-secondary, #f5f5f5);
215
- border-radius: 24px;
216
- padding: 12px 16px;
217
- padding-right: 16px; /* Remove extra padding - no buttons on right anymore */
218
- min-height: 44px;
219
- position: relative;
220
- }
221
-
222
- /* Mention menu custom styles */
223
- .mention-user-info {
224
- display: flex;
225
- flex-direction: column;
226
- gap: 2px;
227
- flex: 1;
228
- min-width: 0;
229
- }
230
-
231
- .mention-user-name {
232
- font-family: 'Brockmann', sans-serif;
233
- font-size: var(--font-size-base);
234
- font-weight: 600;
235
- line-height: 20px;
236
- color: var(--color-text-primary, #1a1a1a);
237
- }
238
-
239
- .mention-user-role {
240
- font-family: 'Brockmann', sans-serif;
241
- font-size: var(--font-size-sm);
242
- line-height: 18px;
243
- color: var(--color-text-secondary, #737373);
244
- }
245
-
246
- .composer-input {
247
- flex: 1;
248
- border: none;
249
- background: transparent;
250
- font-family: 'Brockmann', sans-serif;
251
- font-size: var(--font-size-sm);
252
- line-height: 20px;
253
- color: var(--color-text-primary, #1a1a1a);
254
- outline: none;
255
- resize: none;
256
- min-height: 20px;
257
- max-height: 120px;
258
- overflow-y: auto;
259
- padding: 0;
260
- margin: 0;
261
- }
262
-
263
- .composer-input::placeholder {
264
- color: var(--color-text-tertiary, #a0a0a0);
265
- font-size: var(--font-size-sm);
266
- }
267
-
268
- /* Send button - positioned absolute in top right corner like comment composer */
269
- .send-button-inline {
270
- position: absolute;
271
- top: 6px;
272
- right: 6px;
273
- z-index: 10;
274
- flex-shrink: 0;
275
- opacity: 0;
276
- transform: translateX(20px) scale(0.8);
277
- pointer-events: none;
278
- transition:
279
- opacity 0.15s ease-in,
280
- transform 0.15s ease-in;
281
- }
282
-
283
- .send-button-inline.show {
284
- opacity: 1;
285
- transform: translateX(0) scale(1);
286
- pointer-events: auto;
287
- animation: slideInFromRight var(--spring-bouncy);
288
- }
289
-
290
- .send-button-inline::ng-deep button {
291
- width: 32px !important;
292
- height: 32px !important;
293
- min-width: 32px !important;
294
- min-height: 32px !important;
295
- padding: 0 !important;
296
- border-radius: 50% !important;
297
- }
298
-
299
- @keyframes slideInFromRight {
300
- from {
301
- opacity: 0;
302
- transform: translateX(20px) scale(0.8);
303
- }
304
- to {
305
- opacity: 1;
306
- transform: translateX(0) scale(1);
307
- }
308
- }
309
- `,
310
- ],
311
- template: `
312
- <div class="message-composer">
313
- <!-- Edit indicator (optional) -->
314
- @if (editingMessage()) {
315
- <div class="edit-indicator">
316
- <div class="edit-indicator-content">
317
- <ds-icon name="remixEditLine" size="16px" />
318
- <span class="edit-text">{{ editIndicatorText() }}</span>
319
- </div>
320
- <button class="cancel-edit" (click)="cancelEdit()" type="button">
321
- <ds-icon name="remixCloseLine" size="16px" />
322
- </button>
323
- </div>
324
- }
325
-
326
- <!-- Reply indicator (optional) -->
327
- @if (replyingTo() && !editingMessage()) {
328
- <div class="reply-indicator">
329
- <div class="reply-indicator-content">
330
- <ds-icon name="remixReplyLine" size="16px" />
331
- <span class="reply-to-text">
332
- {{ replyIndicatorText() }} <span class="reply-author">{{ replyingTo()!.authorName }}</span>
333
- </span>
334
- </div>
335
- <button class="cancel-reply" (click)="cancelReply()" type="button">
336
- <ds-icon name="remixCloseLine" size="16px" />
337
- </button>
338
- </div>
339
- }
340
-
341
- <!-- Attachment Previews (if any) -->
342
- @if (attachments().length > 0) {
343
- <div class="attachment-previews-section">
344
- <div class="attachment-previews">
345
- @for (attachment of attachments(); track attachment.id) {
346
- <ds-mobile-attachment-preview [attachment]="attachment" (remove)="removeAttachment($event)" />
347
- }
348
- </div>
349
- </div>
350
- }
351
-
352
- <div class="composer-content">
353
- <!-- Attachment button replacing avatar (left side) -->
354
- @if (showAttachmentButton()) {
355
- <div class="composer-leading-button" [class.composer-leading-button--open]="isAttachmentMenuOpen()">
356
- <!-- Main attachment button -->
357
- <ds-icon-button
358
- id="attachment-trigger"
359
- icon="remixAddLine"
360
- variant="secondary"
361
- size="lg"
362
- (clicked)="toggleAttachmentMenu($event)"
363
- [attr.aria-label]="attachmentButtonLabel()"
364
- [attr.aria-expanded]="isAttachmentMenuOpen()"
365
- >
366
- </ds-icon-button>
367
-
368
- <!-- Attachment menu using dropdown -->
369
- <ds-mobile-dropdown
370
- [items]="attachmentMenuItems"
371
- [isOpen]="isAttachmentMenuOpen()"
372
- [trigger]="'attachment-trigger'"
373
- [keepFocusOn]="messageInputRef"
374
- position="above"
375
- align="start"
376
- (itemSelected)="handleAttachmentMenuSelect($event)"
377
- (closed)="closeAttachmentMenu()"
378
- >
379
- </ds-mobile-dropdown>
380
- </div>
381
- } @else {
382
- <!-- Avatar (only shown when attachment button is hidden) -->
383
- <ds-avatar [initials]="avatarInitials()" [type]="avatarType()" [src]="avatarSrc()" size="lg" />
384
- }
385
-
386
- <div class="composer-input-wrapper">
387
- <textarea
388
- #messageInputEl
389
- id="message-input-trigger"
390
- class="composer-input"
391
- [placeholder]="placeholder()"
392
- [(ngModel)]="messageText"
393
- (input)="handleInput($event)"
394
- (keydown)="handleKeyDown($event)"
395
- (focus)="showKeyboard()"
396
- (click)="showKeyboard()"
397
- rows="1"
398
- >
399
- </textarea>
400
-
401
- <!-- Mention menu using dropdown (only render if mentions are enabled) -->
402
- @if (enableMentions()) {
403
- <ds-mobile-dropdown
404
- [items]="mentionDropdownItems()"
405
- [isOpen]="showMentionMenu() && !editingMessage() && mentionDropdownItems().length > 0"
406
- [trigger]="'message-input-trigger'"
407
- position="above"
408
- align="start"
409
- [maxHeight]="200"
410
- (itemSelected)="handleMentionSelect($event)"
411
- (closed)="closeMentionMenu()"
412
- >
413
- <ng-template #itemTemplate let-item>
414
- <ds-avatar [initials]="item.data.initials" [type]="'initials'" size="sm" />
415
- <div class="mention-user-info">
416
- <span class="mention-user-name">{{ item.data.name }}</span>
417
- <span class="mention-user-role">{{ item.data.role }}</span>
418
- </div>
419
- </ng-template>
420
- </ds-mobile-dropdown>
421
- }
422
-
423
- <!-- Send button (absolute positioned in top right, always rendered) -->
424
- <ds-icon-button
425
- icon="remixCheckLine"
426
- variant="primary"
427
- size="sm"
428
- (clicked)="sendMessage()"
429
- [attr.aria-label]="sendButtonLabel()"
430
- [class.send-button-inline]="true"
431
- [class.show]="messageText().trim().length > 0 || attachments().length > 0"
432
- >
433
- </ds-icon-button>
434
- </div>
435
- </div>
436
-
437
- <!-- Hidden file input -->
438
- <input
439
- #fileInput
440
- type="file"
441
- accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.csv,.zip,.rar"
442
- multiple
443
- aria-hidden="true"
444
- style="display: none;"
445
- (change)="handleFileSelect($event)"
446
- />
447
- </div>
448
- `,
449
- })
450
- export class DsMobileMessageComposerComponent implements AfterViewInit, OnDestroy {
451
- constructor(private cdr: ChangeDetectorRef) {}
452
-
453
- /**
454
- * Avatar initials
455
- */
456
- avatarInitials = input<string>('');
457
-
458
- /**
459
- * Avatar type
460
- */
461
- avatarType = input<'initials' | 'photo' | 'icon'>('initials');
462
-
463
- /**
464
- * Avatar photo source (for photo type)
465
- */
466
- avatarSrc = input<string>('');
467
-
468
- /**
469
- * Placeholder text for the input
470
- */
471
- placeholder = input<string>('Write a message...');
472
-
473
- /**
474
- * Send button aria label
475
- */
476
- sendButtonLabel = input<string>('Send message');
477
-
478
- /**
479
- * Attachment button aria label
480
- */
481
- attachmentButtonLabel = input<string>('Add attachment');
482
-
483
- /**
484
- * Whether to show the attachment button
485
- */
486
- showAttachmentButton = input<boolean>(false);
487
-
488
- /**
489
- * Edit indicator text (when editing)
490
- */
491
- editIndicatorText = input<string>('Editing message');
492
-
493
- /**
494
- * Reply indicator text prefix
495
- */
496
- replyIndicatorText = input<string>('Replying to');
497
-
498
- /**
499
- * Whether to enable mention support
500
- */
501
- enableMentions = input<boolean>(false);
502
-
503
- /**
504
- * Available users for mentions (if mentions enabled)
505
- */
506
- mentionUsers = input<Array<{ name: string; initials: string; role: string }>>([]);
507
-
508
- /**
509
- * Auto-focus input on mount
510
- */
511
- autoFocus = input<boolean>(false);
512
-
513
- /**
514
- * ViewChild for message input
515
- */
516
- @ViewChild('messageInputEl') messageInputRef?: ElementRef<HTMLTextAreaElement>;
517
-
518
- /**
519
- * ViewChild for file input
520
- */
521
- @ViewChild('fileInput') fileInput?: ElementRef<HTMLInputElement>;
522
-
523
- /**
524
- * Message text signal
525
- */
526
- messageText = signal('');
527
-
528
- /**
529
- * Attachments signal
530
- */
531
- attachments = signal<AttachmentData[]>([]);
532
-
533
- /**
534
- * Attachment menu open state
535
- */
536
- isAttachmentMenuOpen = signal(false);
537
-
538
- /**
539
- * Editing message state (optional)
540
- */
541
- editingMessage = signal<{ authorName: string; originalContent: string; timestamp: string } | null>(null);
542
-
543
- /**
544
- * Replying to state (optional)
545
- */
546
- replyingTo = signal<{ authorName: string; content: string } | null>(null);
547
-
548
- /**
549
- * Mention menu state
550
- */
551
- showMentionMenu = signal(false);
552
-
553
- /**
554
- * Mention query for filtering
555
- */
556
- mentionQuery = signal('');
557
-
558
- /**
559
- * Filtered users based on mention query
560
- */
561
- filteredUsers = computed(() => {
562
- if (!this.enableMentions()) return [];
563
-
564
- const query = this.mentionQuery().toLowerCase();
565
- const users = this.mentionUsers();
566
-
567
- if (!query) return users;
568
- return users.filter((user) => user.name.toLowerCase().includes(query));
569
- });
570
-
571
- /**
572
- * Convert filtered users to dropdown items
573
- */
574
- mentionDropdownItems = computed((): DsMobileDropdownItem[] => {
575
- return this.filteredUsers().map((user) => ({
576
- id: user.name,
577
- label: user.name,
578
- data: {
579
- name: user.name,
580
- initials: user.initials,
581
- role: user.role,
582
- },
583
- }));
584
- });
585
-
586
- /**
587
- * Attachment menu items
588
- * Static list to prevent change detection loops
589
- */
590
- readonly attachmentMenuItems: DsMobileDropdownItem[] = [
591
- {
592
- id: 'photo',
593
- leadingIcon: 'remixImageLine',
594
- label: 'Photo',
595
- action: () => this.handleAddPhoto(),
596
- },
597
- {
598
- id: 'file',
599
- leadingIcon: 'remixFile3Line',
600
- label: 'File',
601
- action: () => this.handleAddFile(),
602
- },
603
- ];
604
-
605
- /**
606
- * Emits when a message is sent
607
- */
608
- messageSent = output<{
609
- content: string;
610
- isReply?: boolean;
611
- replyTo?: string;
612
- isEdit?: boolean;
613
- attachments?: AttachmentData[];
614
- }>();
615
-
616
- /**
617
- * Emits when edit is cancelled
618
- */
619
- editCancelled = output<void>();
620
-
621
- /**
622
- * Emits when reply is cancelled
623
- */
624
- replyCancelled = output<void>();
625
-
626
- /**
627
- * Emits when mention is selected
628
- */
629
- mentionSelected = output<{ userName: string }>();
630
-
631
- /**
632
- * Emits when attachment button is clicked
633
- */
634
- attachmentClicked = output<void>();
635
-
636
- /**
637
- * Emits when attachments array changes (added or removed)
638
- * Parent components (like chat modal) can use this to scroll to bottom
639
- */
640
- attachmentsChanged = output<void>();
641
-
642
- ngAfterViewInit(): void {
643
- // Auto-focus input if requested
644
- if (this.autoFocus()) {
645
- setTimeout(() => {
646
- this.messageInputRef?.nativeElement.focus();
647
- this.showKeyboard();
648
- }, 300);
649
- }
650
-
651
- // Set up keyboard listeners
652
- this.setupKeyboardListeners();
653
-
654
- // Explicitly trigger change detection to avoid NG0100 with ViewChild bindings
655
- this.cdr.detectChanges();
656
- }
657
-
658
- ngOnDestroy(): void {
659
- // Clean up keyboard listeners
660
- this.cleanupKeyboardListeners();
661
- }
662
-
663
- /**
664
- * Set up keyboard event listeners
665
- */
666
- private setupKeyboardListeners(): void {
667
- Keyboard.addListener('keyboardWillShow', (info) => {
668
- document.documentElement.style.setProperty('--keyboard-height', `${info.keyboardHeight}px`);
669
- }).catch(() => {});
670
-
671
- Keyboard.addListener('keyboardWillHide', () => {
672
- document.documentElement.style.setProperty('--keyboard-height', '0px');
673
- }).catch(() => {});
674
- }
675
-
676
- /**
677
- * Clean up keyboard event listeners
678
- */
679
- private cleanupKeyboardListeners(): void {
680
- Keyboard.removeAllListeners().catch(() => {});
681
- }
682
-
683
- /**
684
- * Show the keyboard when user interacts with input
685
- */
686
- showKeyboard(): void {
687
- Keyboard.show().catch(() => {});
688
- }
689
-
690
- /**
691
- * Handle keyboard shortcuts (Shift+Enter to send)
692
- */
693
- handleKeyDown(event: KeyboardEvent): void {
694
- // Shift+Enter sends the message
695
- if (event.key === 'Enter' && event.shiftKey) {
696
- event.preventDefault();
697
- const hasContent = this.messageText().trim().length > 0 || this.attachments().length > 0;
698
- if (hasContent) {
699
- this.sendMessage();
700
- }
701
- }
702
- // Regular Enter just creates a new line (default behavior)
703
- }
704
-
705
- /**
706
- * Handle input changes and detect @ mentions
707
- */
708
- handleInput(event: Event): void {
709
- const textarea = event.target as HTMLTextAreaElement;
710
- const text = textarea.value;
711
- const cursorPosition = textarea.selectionStart || 0;
712
-
713
- // Update signal
714
- this.messageText.set(text);
715
-
716
- // Auto-resize textarea
717
- textarea.style.height = 'auto';
718
- textarea.style.height = textarea.scrollHeight + 'px';
719
-
720
- // Handle mentions if enabled
721
- if (this.enableMentions()) {
722
- // Find the last @ before cursor
723
- const textBeforeCursor = text.substring(0, cursorPosition);
724
- const lastAtIndex = textBeforeCursor.lastIndexOf('@');
725
-
726
- if (lastAtIndex !== -1) {
727
- // Check if there's a space after @
728
- const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
729
- const hasSpace = textAfterAt.includes(' ');
730
-
731
- if (!hasSpace) {
732
- // Show mention menu
733
- this.showMentionMenu.set(true);
734
- this.mentionQuery.set(textAfterAt);
735
- } else {
736
- this.showMentionMenu.set(false);
737
- }
738
- } else {
739
- this.showMentionMenu.set(false);
740
- }
741
- }
742
- }
743
-
744
- /**
745
- * Handle mention selection from dropdown
746
- */
747
- handleMentionSelect(item: DsMobileDropdownItem): void {
748
- this.selectMention(item.data.name);
749
- }
750
-
751
- /**
752
- * Close mention menu
753
- */
754
- closeMentionMenu(): void {
755
- this.showMentionMenu.set(false);
756
- }
757
-
758
- /**
759
- * Handle attachment menu selection from dropdown
760
- */
761
- handleAttachmentMenuSelect(item: DsMobileDropdownItem): void {
762
- // Action is already called by the dropdown, just close the menu
763
- this.closeAttachmentMenu();
764
- }
765
-
766
- /**
767
- * Select a user from mention menu
768
- */
769
- selectMention(userName: string): void {
770
- // Set as reply (similar to clicking Reply)
771
- this.replyingTo.set({ authorName: userName, content: '' });
772
-
773
- // Clear the @ from the input
774
- const currentText = this.messageText();
775
- const textWithoutMention = currentText.substring(0, currentText.lastIndexOf('@'));
776
- this.messageText.set(textWithoutMention);
777
-
778
- // Hide mention menu
779
- this.showMentionMenu.set(false);
780
-
781
- // Emit mention selected event
782
- this.mentionSelected.emit({ userName });
783
-
784
- // Focus back on input
785
- setTimeout(() => {
786
- this.messageInputRef?.nativeElement.focus();
787
- }, 0);
788
- }
789
-
790
- /**
791
- * Cancel edit
792
- */
793
- cancelEdit(): void {
794
- this.editingMessage.set(null);
795
- this.messageText.set('');
796
- this.editCancelled.emit();
797
- }
798
-
799
- /**
800
- * Cancel reply
801
- */
802
- cancelReply(): void {
803
- this.replyingTo.set(null);
804
- this.replyCancelled.emit();
805
- }
806
-
807
- /**
808
- * Set reply state (for external use)
809
- */
810
- setReply(authorName: string, content: string): void {
811
- this.replyingTo.set({ authorName, content });
812
- // Focus the input and show keyboard
813
- setTimeout(() => {
814
- this.messageInputRef?.nativeElement.focus();
815
- this.showKeyboard();
816
- }, 100);
817
- }
818
-
819
- /**
820
- * Set edit state (for external use)
821
- */
822
- setEdit(authorName: string, originalContent: string, timestamp: string): void {
823
- // Clear reply state if active
824
- this.replyingTo.set(null);
825
-
826
- // Remove @mention from the content if it exists
827
- let contentToEdit = originalContent;
828
- const mentionMatch = originalContent.match(/^@([A-Za-z]+(?:\s+[A-Za-z]+)?)\s+/);
829
- if (mentionMatch) {
830
- contentToEdit = originalContent.substring(mentionMatch[0].length);
831
- }
832
-
833
- // Set edit state
834
- this.editingMessage.set({ authorName, originalContent, timestamp });
835
-
836
- // Populate the input with existing content
837
- this.messageText.set(contentToEdit);
838
-
839
- // Focus the input, show keyboard, and auto-resize
840
- setTimeout(() => {
841
- if (this.messageInputRef?.nativeElement) {
842
- const textarea = this.messageInputRef.nativeElement;
843
- textarea.focus();
844
-
845
- // Auto-resize textarea to fit content
846
- textarea.style.height = 'auto';
847
- textarea.style.height = textarea.scrollHeight + 'px';
848
-
849
- this.showKeyboard();
850
- }
851
- }, 100);
852
- }
853
-
854
- /**
855
- * Clear composer state
856
- */
857
- clear(): void {
858
- this.messageText.set('');
859
- this.editingMessage.set(null);
860
- this.replyingTo.set(null);
861
- this.showMentionMenu.set(false);
862
- this.attachments.set([]);
863
-
864
- // Reset textarea height
865
- if (this.messageInputRef?.nativeElement) {
866
- this.messageInputRef.nativeElement.style.height = 'auto';
867
- }
868
- }
869
-
870
- /**
871
- * Focus the input
872
- */
873
- focus(): void {
874
- this.messageInputRef?.nativeElement.focus();
875
- this.showKeyboard();
876
- }
877
-
878
- /**
879
- * Toggle attachment menu open/closed
880
- * Uses mousedown/touchstart to prevent focus loss from textarea
881
- */
882
- toggleAttachmentMenu(event?: MouseEvent | TouchEvent): void {
883
- if (event) {
884
- event.preventDefault();
885
- event.stopPropagation();
886
- }
887
- this.isAttachmentMenuOpen.update((open) => !open);
888
- }
889
-
890
- /**
891
- * Close attachment menu
892
- */
893
- closeAttachmentMenu(event?: MouseEvent): void {
894
- if (event) {
895
- event.preventDefault();
896
- event.stopPropagation();
897
- }
898
- this.isAttachmentMenuOpen.set(false);
899
- }
900
-
901
- /**
902
- * Handle add photo button click from menu
903
- * Uses Capawesome File Picker API to open photo library directly
904
- * Allows multiple photo selection
905
- */
906
- async handleAddPhoto(event?: MouseEvent): Promise<void> {
907
- if (event) {
908
- event.preventDefault();
909
- event.stopPropagation();
910
- }
911
-
912
- if (this.attachments().length >= 6) {
913
- return;
914
- }
915
-
916
- try {
917
- //console.log('[MessageComposer] Opening photo library');
918
-
919
- // Calculate remaining slots
920
- const remainingSlots = 6 - this.attachments().length;
921
-
922
- // Open photo library with multiple selection using pickImages
923
- const result = await FilePicker.pickImages({
924
- limit: remainingSlots, // Limit to remaining slots
925
- });
926
-
927
- if (result.files && result.files.length > 0) {
928
- //console.log(`[MessageComposer] ${result.files.length} photo(s) selected`);
929
-
930
- // Process each selected photo
931
- for (const photo of result.files) {
932
- const attachmentId = `photo-${Date.now()}-${Math.random()}`;
933
-
934
- // Add attachment with loading state
935
- const loadingAttachment: AttachmentData = {
936
- id: attachmentId,
937
- src: photo.path ? Capacitor.convertFileSrc(photo.path) : (photo.blob ? URL.createObjectURL(photo.blob) : ''),
938
- type: 'image',
939
- name: photo.name,
940
- size: this.formatFileSize(photo.size ?? 0),
941
- isLoading: true,
942
- };
943
-
944
- this.attachments.update((attachments) => [...attachments, loadingAttachment]);
945
-
946
- // Simulate processing time (in real app, this would be actual image processing)
947
- // TODO: Reduce to 300ms or remove in production
948
- setTimeout(() => {
949
- this.attachments.update((attachments) => attachments.map((a) => (a.id === attachmentId ? { ...a, isLoading: false } : a)));
950
- }, 1500); // 1.5s for testing - shows loading overlay clearly
951
- }
952
-
953
- //console.log('[MessageComposer] All photos added successfully');
954
-
955
- // Notify parent that attachments changed so it can scroll
956
- this.attachmentsChanged.emit();
957
-
958
- // ResizeObserver in MobileModalBase automatically handles layout adjustments
959
- }
960
- } catch (error: any) {
961
- if (error.message && !error.message.includes('cancel')) {
962
- //console.error('[MessageComposer] Error adding photo:', error);
963
- }
964
- // User cancelled - that's fine
965
- }
966
- }
967
-
968
- /**
969
- * Handle add file button click from menu
970
- * Opens file picker
971
- */
972
- handleAddFile(event?: MouseEvent): void {
973
- if (event) {
974
- event.preventDefault();
975
- event.stopPropagation();
976
- }
977
-
978
- if (this.attachments().length >= 6) {
979
- return;
980
- }
981
-
982
- console.log('[MessageComposer] Opening file picker');
983
-
984
- // Trigger the hidden file input
985
- if (this.fileInput) {
986
- this.fileInput.nativeElement.click();
987
- }
988
- this.attachmentClicked.emit();
989
- }
990
-
991
- /**
992
- * Detect file type from file name or mime type
993
- */
994
- private detectFileType(file: File): AttachmentFileType {
995
- const fileName = file.name.toLowerCase();
996
- const mimeType = file.type.toLowerCase();
997
-
998
- // Check if it's an image
999
- if (mimeType.startsWith('image/')) {
1000
- return 'image';
1001
- }
1002
-
1003
- // Check file extension
1004
- if (fileName.endsWith('.pdf')) {
1005
- return 'pdf';
1006
- } else if (fileName.endsWith('.doc')) {
1007
- return 'doc';
1008
- } else if (fileName.endsWith('.docx')) {
1009
- return 'docx';
1010
- } else if (fileName.endsWith('.xls')) {
1011
- return 'xls';
1012
- } else if (fileName.endsWith('.xlsx')) {
1013
- return 'xlsx';
1014
- }
1015
-
1016
- return 'other';
1017
- }
1018
-
1019
- /**
1020
- * Format file size for display
1021
- */
1022
- private formatFileSize(bytes: number): string {
1023
- if (bytes === 0) return '0 B';
1024
- const k = 1024;
1025
- const sizes = ['B', 'KB', 'MB', 'GB'];
1026
- const i = Math.floor(Math.log(bytes) / Math.log(k));
1027
- return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
1028
- }
1029
-
1030
- /**
1031
- * Handle file selection from file input
1032
- */
1033
- handleFileSelect(event: Event): void {
1034
- const input = event.target as HTMLInputElement;
1035
- const files = input.files;
1036
-
1037
- if (!files || files.length === 0) {
1038
- // User cancelled - blur handler will restore keyboard
1039
- return;
1040
- }
1041
-
1042
- // Process each selected file (up to 6 total)
1043
- const remainingSlots = 6 - this.attachments().length;
1044
- const filesToProcess = Array.from(files).slice(0, remainingSlots);
1045
-
1046
- filesToProcess.forEach((file) => {
1047
- const fileType = this.detectFileType(file);
1048
- const attachmentId = `file-${Date.now()}-${Math.random()}`;
1049
-
1050
- // Add attachment with loading state immediately
1051
- const loadingAttachment: AttachmentData = {
1052
- id: attachmentId,
1053
- src: '', // Will be set after reading
1054
- type: fileType,
1055
- name: file.name,
1056
- size: this.formatFileSize(file.size),
1057
- isLoading: true,
1058
- };
1059
- this.attachments.update((attachments) => [...attachments, loadingAttachment]);
1060
-
1061
- // Create a data URL for preview
1062
- const reader = new FileReader();
1063
- reader.onload = (e) => {
1064
- const result = e.target?.result as string;
1065
- if (result) {
1066
- // Add minimum loading time for better UX feedback
1067
- // TODO: Remove setTimeout in production (use actual FileReader timing)
1068
- setTimeout(() => {
1069
- // Update attachment with actual data and remove loading state
1070
- this.attachments.update((attachments) => attachments.map((a) => (a.id === attachmentId ? { ...a, src: result, isLoading: false } : a)));
1071
-
1072
- // Notify parent that attachments changed so it can scroll
1073
- setTimeout(() => {
1074
- this.attachmentsChanged.emit();
1075
- }, 100);
1076
- }, 1000); // 1s minimum for testing - shows loading overlay clearly
1077
- }
1078
- };
1079
- reader.readAsDataURL(file);
1080
- });
1081
-
1082
- // Reset the input so the same file can be selected again
1083
- input.value = '';
1084
-
1085
- // ResizeObserver in MobileModalBase automatically handles layout adjustments
1086
- }
1087
-
1088
- /**
1089
- * Remove an attachment from the list
1090
- * Keeps keyboard open by maintaining focus
1091
- */
1092
- removeAttachment(attachmentId: string): void {
1093
- this.attachments.update((attachments) => attachments.filter((a) => a.id !== attachmentId));
1094
-
1095
- // Immediately refocus input to prevent keyboard from closing
1096
- setTimeout(() => {
1097
- if (this.messageInputRef?.nativeElement) {
1098
- this.messageInputRef.nativeElement.focus();
1099
- Keyboard.show().catch((e) => console.log('Keyboard.show() not available:', e));
1100
- }
1101
- }, 0);
1102
-
1103
- // Notify parent that attachments changed so it can scroll
1104
- this.attachmentsChanged.emit();
1105
-
1106
- // ResizeObserver in MobileModalBase automatically handles layout adjustments
1107
- }
1108
-
1109
- /**
1110
- * Send message
1111
- */
1112
- sendMessage(): void {
1113
- const text = this.messageText().trim();
1114
- const hasAttachments = this.attachments().length > 0;
1115
-
1116
- // Must have either text or attachments
1117
- if (!text && !hasAttachments) return;
1118
-
1119
- const isEdit = !!this.editingMessage();
1120
- const isReply = !!this.replyingTo();
1121
-
1122
- // Emit message sent event
1123
- this.messageSent.emit({
1124
- content: isReply && this.replyingTo() ? `@${this.replyingTo()!.authorName} ${text}` : text,
1125
- isReply,
1126
- replyTo: this.replyingTo()?.authorName,
1127
- isEdit,
1128
- attachments: hasAttachments ? [...this.attachments()] : undefined,
1129
- });
1130
-
1131
- // Keep keyboard open by explicitly showing it before clearing
1132
- // This prevents the keyboard from starting to close during the clear operation
1133
- Keyboard.show().catch(() => {
1134
- // Keyboard.show() not available on web, that's fine
1135
- });
1136
-
1137
- // Clear the input and states
1138
- this.clear();
1139
-
1140
- // Ensure input stays focused for quick follow-up messages
1141
- // The keyboard should remain open throughout this process
1142
- if (this.messageInputRef?.nativeElement) {
1143
- this.messageInputRef.nativeElement.focus();
1144
- }
1145
- }
1146
- }