@propbinder/mobile-design 0.2.48 → 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.
- package/ng-package.json +24 -0
- package/package.json +3 -39
- package/src/animations/page-transitions.ts +165 -0
- package/src/assets/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
- package/src/components/action-list-item/ds-mobile-action-list-item.ts +102 -0
- package/src/components/action-list-item/index.ts +2 -0
- package/src/components/app-icon/ds-app-icon.ts +133 -0
- package/src/components/app-icon/index.ts +2 -0
- package/src/components/attachment-preview/ds-mobile-attachment-preview.css +139 -0
- package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +164 -0
- package/src/components/attachment-preview/index.ts +1 -0
- package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +142 -0
- package/src/components/avatar-with-badge/index.ts +2 -0
- package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +71 -0
- package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +121 -0
- package/src/components/booking-modal/ds-mobile-booking-modal.ts +598 -0
- package/src/components/booking-modal/ds-mobile-booking-summary.ts +161 -0
- package/src/components/booking-modal/index.ts +4 -0
- package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +266 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +146 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +156 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +101 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +169 -0
- package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +211 -0
- package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +578 -0
- package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +614 -0
- package/src/components/bottom-sheet/index.ts +8 -0
- package/src/components/bottom-sheet/modal-shadow-fix.ts +42 -0
- package/src/components/card-inline/ds-mobile-card-inline.ts +301 -0
- package/src/components/card-inline/index.ts +2 -0
- package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +118 -0
- package/src/components/card-inline-banner/index.ts +1 -0
- package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +120 -0
- package/src/components/card-inline-contact/index.ts +1 -0
- package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +141 -0
- package/src/components/card-inline-file/index.ts +1 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.css +159 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +105 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.ts +918 -0
- package/src/components/chat-modal/index.ts +8 -0
- package/src/components/comment/ds-mobile-comment.ts +568 -0
- package/src/components/comment/index.ts +2 -0
- package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
- package/src/components/contact-list-item/index.ts +2 -0
- package/src/components/content/ds-mobile-content.ts +139 -0
- package/src/components/content/index.ts +2 -0
- package/src/components/dropdown/ds-mobile-dropdown.css +199 -0
- package/src/components/dropdown/ds-mobile-dropdown.ts +340 -0
- package/src/components/dropdown/index.ts +2 -0
- package/src/components/ds-mobile-tabs.css +407 -0
- package/src/components/ds-mobile-tabs.ts +216 -0
- package/src/components/empty-state/ds-mobile-empty-state.ts +120 -0
- package/src/components/empty-state/index.ts +2 -0
- package/src/components/fab/ds-mobile-fab.ts +315 -0
- package/src/components/fab/index.ts +1 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +121 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +189 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +135 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +656 -0
- package/src/components/facility-creation-modal/index.ts +9 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +105 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +188 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +460 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +134 -0
- package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +69 -0
- package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +379 -0
- package/src/components/facility-detail-modal/index.ts +2 -0
- package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
- package/src/components/file-attachment/index.ts +2 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +214 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +84 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +424 -0
- package/src/components/handbook-detail-modal/index.ts +3 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +175 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +533 -0
- package/src/components/handbook-folder/index.ts +4 -0
- package/src/components/header-content/ds-mobile-header-content.ts +222 -0
- package/src/components/header-content/index.ts +2 -0
- package/src/components/illustration/ds-mobile-illustration.ts +124 -0
- package/src/components/illustration/index.ts +2 -0
- package/src/components/index.ts +124 -0
- package/src/components/inline-photo/ds-mobile-inline-photo.ts +361 -0
- package/src/components/inline-photo/index.ts +1 -0
- package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +132 -0
- package/src/components/inline-tabs/index.ts +2 -0
- package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +350 -0
- package/src/components/interactive-list-item-booking/index.ts +1 -0
- package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +321 -0
- package/src/components/interactive-list-item-inquiry/index.ts +2 -0
- package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +237 -0
- package/src/components/interactive-list-item-message/index.ts +2 -0
- package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +549 -0
- package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
- package/src/components/interactive-list-item-post/index.ts +13 -0
- package/src/components/lightbox/ds-mobile-lightbox-footer.ts +315 -0
- package/src/components/lightbox/ds-mobile-lightbox-header.ts +202 -0
- package/src/components/lightbox/ds-mobile-lightbox-image.ts +484 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.css +377 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
- package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
- package/src/components/lightbox/ds-mobile-lightbox.service.ts +296 -0
- package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
- package/src/components/lightbox/index.ts +22 -0
- package/src/components/list-item/ds-mobile-list-item.ts +603 -0
- package/src/components/list-item/index.ts +2 -0
- package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
- package/src/components/list-item-static/index.ts +2 -0
- package/src/components/loader-overlay/ds-mobile-loader-overlay.css +49 -0
- package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +77 -0
- package/src/components/loader-overlay/index.ts +1 -0
- package/src/components/logo/ds-logo.ts +95 -0
- package/src/components/logo/index.ts +2 -0
- package/src/components/message-bubble/ds-mobile-message-bubble.ts +633 -0
- package/src/components/message-bubble/index.ts +7 -0
- package/src/components/message-composer/ds-mobile-message-composer.ts +1146 -0
- package/src/components/message-composer/index.ts +7 -0
- package/src/components/modal/ds-mobile-modal.css +163 -0
- package/src/components/modal/ds-mobile-modal.service.ts +329 -0
- package/src/components/modal/index.ts +8 -0
- package/src/components/modal-base/ds-mobile-modal-base.css +378 -0
- package/src/components/modal-base/ds-mobile-modal-base.ts +261 -0
- package/src/components/modal-base/index.ts +2 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +112 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +93 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +442 -0
- package/src/components/new-inquiry-modal/index.ts +4 -0
- package/src/components/offline-banner/ds-mobile-offline-banner.ts +135 -0
- package/src/components/offline-banner/index.ts +1 -0
- package/src/components/page-details/ds-mobile-page-details.css +83 -0
- package/src/components/page-details/ds-mobile-page-details.ts +282 -0
- package/src/components/page-details/index.ts +2 -0
- package/src/components/page-main/ds-mobile-page-main.css +68 -0
- package/src/components/page-main/ds-mobile-page-main.ts +421 -0
- package/src/components/page-main/index.ts +2 -0
- package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
- package/src/components/post-composer/index.ts +2 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +390 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +108 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +722 -0
- package/src/components/post-detail-modal/index.ts +9 -0
- package/src/components/property-banner/ds-mobile-property-banner.ts +95 -0
- package/src/components/property-banner/index.ts +2 -0
- package/src/components/section/ds-mobile-section.ts +263 -0
- package/src/components/section/index.ts +2 -0
- package/src/components/shared/directives/index.ts +2 -0
- package/src/components/shared/directives/long-press.directive.ts +212 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/shared/mobile-modal-base.ts +457 -0
- package/src/components/shared/mobile-page-base.ts +204 -0
- package/src/components/swiper/ds-mobile-swiper-with-nav.ts +160 -0
- package/src/components/swiper/ds-mobile-swiper.ts +327 -0
- package/src/components/swiper/index.ts +3 -0
- package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +129 -0
- package/src/components/system-message-banner/index.ts +2 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.css +533 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.ts +735 -0
- package/src/components/tab-bar/index.ts +2 -0
- package/src/components/tabs/ds-mobile-tabs.css +25 -0
- package/src/components/tabs/ds-mobile-tabs.ts +89 -0
- package/src/components/tabs/index.ts +2 -0
- package/src/components/text-input/ds-text-input.ts +287 -0
- package/src/components/text-input/index.ts +2 -0
- package/src/examples/booking.page.ts +434 -0
- package/src/examples/community.page.ts +776 -0
- package/src/examples/handbook.page.ts +324 -0
- package/src/examples/home.page.ts +347 -0
- package/src/examples/index.ts +12 -0
- package/src/examples/inquiries.example.ts +273 -0
- package/src/examples/inquiry-detail.example.css +189 -0
- package/src/examples/inquiry-detail.example.ts +415 -0
- package/src/examples/mobile-tabs-example.component.ts +208 -0
- package/src/examples/post-create.page.ts +311 -0
- package/src/examples/post-detail.page.ts +296 -0
- package/src/examples/sign-in.page.ts +291 -0
- package/src/examples/whitelabel-demo-modal.component.ts +1094 -0
- package/src/examples/whitelabel-demo-modal.service.ts +77 -0
- package/src/models/index.ts +7 -0
- package/src/models/post.model.ts +41 -0
- package/src/pages/community.page.ts +769 -0
- package/src/pages/handbook.page.ts +388 -0
- package/src/pages/home.page.ts +303 -0
- package/src/pages/index.ts +11 -0
- package/src/pages/inquiries.example.ts +273 -0
- package/src/pages/inquiry-detail.example.css +189 -0
- package/src/pages/inquiry-detail.example.ts +415 -0
- package/src/pages/mobile-tabs-example.component.ts +179 -0
- package/src/pages/post-create.page.ts +311 -0
- package/src/pages/post-detail.page.ts +296 -0
- package/src/pages/sign-in.page.ts +291 -0
- package/src/pages/whitelabel-demo-modal.component.ts +1094 -0
- package/src/pages/whitelabel-demo-modal.service.ts +77 -0
- package/src/public-api.ts +6 -0
- package/src/services/base-modal.service.ts +101 -0
- package/src/services/index.ts +11 -0
- package/src/services/posts.service.ts +542 -0
- package/src/services/tracking-permission.service.ts +88 -0
- package/src/services/user.service.ts +60 -0
- package/src/services/whitelabel.service.ts +675 -0
- package/{styles → src/styles}/ionic.css +25 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +13 -0
- package/fesm2022/propbinder-mobile-design.mjs +0 -26168
- package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
- package/index.d.ts +0 -8169
- /package/{assets → src/assets}/fonts/Brockmann-Bold.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-BoldItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-Medium.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-MediumItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-Regular.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-RegularItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-SemiBold.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann_desktop_license.pdf +0 -0
- /package/{assets → src/assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
- /package/{assets → src/assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
- /package/{assets → src/assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
- /package/{styles → src/components/shared}/mobile-common.css +0 -0
- /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
|
+
}
|