@propbinder/mobile-design 0.2.47 → 0.2.50

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/ng-package.json +24 -0
  2. package/package.json +3 -39
  3. package/src/animations/page-transitions.ts +165 -0
  4. package/src/assets/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
  5. package/src/assets/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
  6. package/src/assets/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
  7. package/src/components/action-list-item/ds-mobile-action-list-item.ts +102 -0
  8. package/src/components/action-list-item/index.ts +2 -0
  9. package/src/components/app-icon/ds-app-icon.ts +133 -0
  10. package/src/components/app-icon/index.ts +2 -0
  11. package/src/components/attachment-preview/ds-mobile-attachment-preview.css +139 -0
  12. package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +164 -0
  13. package/src/components/attachment-preview/index.ts +1 -0
  14. package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +142 -0
  15. package/src/components/avatar-with-badge/index.ts +2 -0
  16. package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +71 -0
  17. package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +121 -0
  18. package/src/components/booking-modal/ds-mobile-booking-modal.ts +598 -0
  19. package/src/components/booking-modal/ds-mobile-booking-summary.ts +161 -0
  20. package/src/components/booking-modal/index.ts +4 -0
  21. package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +266 -0
  22. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +146 -0
  23. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +156 -0
  24. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +101 -0
  25. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +169 -0
  26. package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +211 -0
  27. package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +578 -0
  28. package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +614 -0
  29. package/src/components/bottom-sheet/index.ts +8 -0
  30. package/src/components/bottom-sheet/modal-shadow-fix.ts +42 -0
  31. package/src/components/card-inline/ds-mobile-card-inline.ts +301 -0
  32. package/src/components/card-inline/index.ts +2 -0
  33. package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +118 -0
  34. package/src/components/card-inline-banner/index.ts +1 -0
  35. package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +120 -0
  36. package/src/components/card-inline-contact/index.ts +1 -0
  37. package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +141 -0
  38. package/src/components/card-inline-file/index.ts +1 -0
  39. package/src/components/chat-modal/ds-mobile-chat-modal.css +159 -0
  40. package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +105 -0
  41. package/src/components/chat-modal/ds-mobile-chat-modal.ts +918 -0
  42. package/src/components/chat-modal/index.ts +8 -0
  43. package/src/components/comment/ds-mobile-comment.ts +568 -0
  44. package/src/components/comment/index.ts +2 -0
  45. package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
  46. package/src/components/contact-list-item/index.ts +2 -0
  47. package/src/components/content/ds-mobile-content.ts +139 -0
  48. package/src/components/content/index.ts +2 -0
  49. package/src/components/dropdown/ds-mobile-dropdown.css +199 -0
  50. package/src/components/dropdown/ds-mobile-dropdown.ts +340 -0
  51. package/src/components/dropdown/index.ts +2 -0
  52. package/src/components/ds-mobile-tabs.css +407 -0
  53. package/src/components/ds-mobile-tabs.ts +216 -0
  54. package/src/components/empty-state/ds-mobile-empty-state.ts +120 -0
  55. package/src/components/empty-state/index.ts +2 -0
  56. package/src/components/fab/ds-mobile-fab.ts +315 -0
  57. package/src/components/fab/index.ts +1 -0
  58. package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +121 -0
  59. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +189 -0
  60. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +135 -0
  61. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +656 -0
  62. package/src/components/facility-creation-modal/index.ts +9 -0
  63. package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +105 -0
  64. package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +188 -0
  65. package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +460 -0
  66. package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +134 -0
  67. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +69 -0
  68. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +379 -0
  69. package/src/components/facility-detail-modal/index.ts +2 -0
  70. package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
  71. package/src/components/file-attachment/index.ts +2 -0
  72. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +214 -0
  73. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +84 -0
  74. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +424 -0
  75. package/src/components/handbook-detail-modal/index.ts +3 -0
  76. package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +175 -0
  77. package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +533 -0
  78. package/src/components/handbook-folder/index.ts +4 -0
  79. package/src/components/header-content/ds-mobile-header-content.ts +222 -0
  80. package/src/components/header-content/index.ts +2 -0
  81. package/src/components/illustration/ds-mobile-illustration.ts +124 -0
  82. package/src/components/illustration/index.ts +2 -0
  83. package/src/components/index.ts +124 -0
  84. package/src/components/inline-photo/ds-mobile-inline-photo.ts +361 -0
  85. package/src/components/inline-photo/index.ts +1 -0
  86. package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +132 -0
  87. package/src/components/inline-tabs/index.ts +2 -0
  88. package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +350 -0
  89. package/src/components/interactive-list-item-booking/index.ts +1 -0
  90. package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +321 -0
  91. package/src/components/interactive-list-item-inquiry/index.ts +2 -0
  92. package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +237 -0
  93. package/src/components/interactive-list-item-message/index.ts +2 -0
  94. package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +549 -0
  95. package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
  96. package/src/components/interactive-list-item-post/index.ts +13 -0
  97. package/src/components/lightbox/ds-mobile-lightbox-footer.ts +315 -0
  98. package/src/components/lightbox/ds-mobile-lightbox-header.ts +202 -0
  99. package/src/components/lightbox/ds-mobile-lightbox-image.ts +484 -0
  100. package/src/components/lightbox/ds-mobile-lightbox-pdf.css +377 -0
  101. package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
  102. package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
  103. package/src/components/lightbox/ds-mobile-lightbox.service.ts +296 -0
  104. package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
  105. package/src/components/lightbox/index.ts +22 -0
  106. package/src/components/list-item/ds-mobile-list-item.ts +603 -0
  107. package/src/components/list-item/index.ts +2 -0
  108. package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
  109. package/src/components/list-item-static/index.ts +2 -0
  110. package/src/components/loader-overlay/ds-mobile-loader-overlay.css +49 -0
  111. package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +77 -0
  112. package/src/components/loader-overlay/index.ts +1 -0
  113. package/src/components/logo/ds-logo.ts +95 -0
  114. package/src/components/logo/index.ts +2 -0
  115. package/src/components/message-bubble/ds-mobile-message-bubble.ts +633 -0
  116. package/src/components/message-bubble/index.ts +7 -0
  117. package/src/components/message-composer/ds-mobile-message-composer.ts +1146 -0
  118. package/src/components/message-composer/index.ts +7 -0
  119. package/src/components/modal/ds-mobile-modal.css +163 -0
  120. package/src/components/modal/ds-mobile-modal.service.ts +329 -0
  121. package/src/components/modal/index.ts +8 -0
  122. package/src/components/modal-base/ds-mobile-modal-base.css +378 -0
  123. package/src/components/modal-base/ds-mobile-modal-base.ts +261 -0
  124. package/src/components/modal-base/index.ts +2 -0
  125. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +112 -0
  126. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +93 -0
  127. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +442 -0
  128. package/src/components/new-inquiry-modal/index.ts +4 -0
  129. package/src/components/offline-banner/ds-mobile-offline-banner.ts +135 -0
  130. package/src/components/offline-banner/index.ts +1 -0
  131. package/src/components/page-details/ds-mobile-page-details.css +83 -0
  132. package/src/components/page-details/ds-mobile-page-details.ts +282 -0
  133. package/src/components/page-details/index.ts +2 -0
  134. package/src/components/page-main/ds-mobile-page-main.css +68 -0
  135. package/src/components/page-main/ds-mobile-page-main.ts +421 -0
  136. package/src/components/page-main/index.ts +2 -0
  137. package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
  138. package/src/components/post-composer/index.ts +2 -0
  139. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +390 -0
  140. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +108 -0
  141. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +722 -0
  142. package/src/components/post-detail-modal/index.ts +9 -0
  143. package/src/components/property-banner/ds-mobile-property-banner.ts +95 -0
  144. package/src/components/property-banner/index.ts +2 -0
  145. package/src/components/section/ds-mobile-section.ts +263 -0
  146. package/src/components/section/index.ts +2 -0
  147. package/src/components/shared/directives/index.ts +2 -0
  148. package/src/components/shared/directives/long-press.directive.ts +212 -0
  149. package/src/components/shared/index.ts +3 -0
  150. package/src/components/shared/mobile-modal-base.ts +457 -0
  151. package/src/components/shared/mobile-page-base.ts +204 -0
  152. package/src/components/swiper/ds-mobile-swiper-with-nav.ts +160 -0
  153. package/src/components/swiper/ds-mobile-swiper.ts +327 -0
  154. package/src/components/swiper/index.ts +3 -0
  155. package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +129 -0
  156. package/src/components/system-message-banner/index.ts +2 -0
  157. package/src/components/tab-bar/ds-mobile-tab-bar.css +533 -0
  158. package/src/components/tab-bar/ds-mobile-tab-bar.ts +735 -0
  159. package/src/components/tab-bar/index.ts +2 -0
  160. package/src/components/tabs/ds-mobile-tabs.css +25 -0
  161. package/src/components/tabs/ds-mobile-tabs.ts +89 -0
  162. package/src/components/tabs/index.ts +2 -0
  163. package/src/components/text-input/ds-text-input.ts +287 -0
  164. package/src/components/text-input/index.ts +2 -0
  165. package/src/examples/booking.page.ts +434 -0
  166. package/src/examples/community.page.ts +776 -0
  167. package/src/examples/handbook.page.ts +324 -0
  168. package/src/examples/home.page.ts +347 -0
  169. package/src/examples/index.ts +12 -0
  170. package/src/examples/inquiries.example.ts +273 -0
  171. package/src/examples/inquiry-detail.example.css +189 -0
  172. package/src/examples/inquiry-detail.example.ts +415 -0
  173. package/src/examples/mobile-tabs-example.component.ts +208 -0
  174. package/src/examples/post-create.page.ts +311 -0
  175. package/src/examples/post-detail.page.ts +296 -0
  176. package/src/examples/sign-in.page.ts +291 -0
  177. package/src/examples/whitelabel-demo-modal.component.ts +1094 -0
  178. package/src/examples/whitelabel-demo-modal.service.ts +77 -0
  179. package/src/models/index.ts +7 -0
  180. package/src/models/post.model.ts +41 -0
  181. package/src/pages/community.page.ts +769 -0
  182. package/src/pages/handbook.page.ts +388 -0
  183. package/src/pages/home.page.ts +303 -0
  184. package/src/pages/index.ts +11 -0
  185. package/src/pages/inquiries.example.ts +273 -0
  186. package/src/pages/inquiry-detail.example.css +189 -0
  187. package/src/pages/inquiry-detail.example.ts +415 -0
  188. package/src/pages/mobile-tabs-example.component.ts +179 -0
  189. package/src/pages/post-create.page.ts +311 -0
  190. package/src/pages/post-detail.page.ts +296 -0
  191. package/src/pages/sign-in.page.ts +291 -0
  192. package/src/pages/whitelabel-demo-modal.component.ts +1094 -0
  193. package/src/pages/whitelabel-demo-modal.service.ts +77 -0
  194. package/src/public-api.ts +6 -0
  195. package/src/services/base-modal.service.ts +101 -0
  196. package/src/services/index.ts +11 -0
  197. package/src/services/posts.service.ts +542 -0
  198. package/src/services/tracking-permission.service.ts +88 -0
  199. package/src/services/user.service.ts +60 -0
  200. package/src/services/whitelabel.service.ts +675 -0
  201. package/{styles → src/styles}/ionic.css +25 -0
  202. package/tsconfig.lib.json +17 -0
  203. package/tsconfig.lib.prod.json +9 -0
  204. package/tsconfig.spec.json +13 -0
  205. package/fesm2022/propbinder-mobile-design.mjs +0 -26136
  206. package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
  207. package/index.d.ts +0 -8154
  208. /package/{assets → src/assets}/fonts/Brockmann-Bold.otf +0 -0
  209. /package/{assets → src/assets}/fonts/Brockmann-BoldItalic.otf +0 -0
  210. /package/{assets → src/assets}/fonts/Brockmann-Medium.otf +0 -0
  211. /package/{assets → src/assets}/fonts/Brockmann-MediumItalic.otf +0 -0
  212. /package/{assets → src/assets}/fonts/Brockmann-Regular.otf +0 -0
  213. /package/{assets → src/assets}/fonts/Brockmann-RegularItalic.otf +0 -0
  214. /package/{assets → src/assets}/fonts/Brockmann-SemiBold.otf +0 -0
  215. /package/{assets → src/assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
  216. /package/{assets → src/assets}/fonts/Brockmann_desktop_license.pdf +0 -0
  217. /package/{assets → src/assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
  218. /package/{assets → src/assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
  219. /package/{assets → src/assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
  220. /package/{styles → src/components/shared}/mobile-common.css +0 -0
  221. /package/{styles → src/components/shared}/mobile-page-base.css +0 -0
@@ -0,0 +1,1146 @@
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
+ }