@propbinder/mobile-design 0.0.1 → 0.0.2
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 +7 -0
- package/package.json +12 -39
- package/src/animations/page-transitions.ts +86 -0
- package/src/assets/fonts/Brockmann-Bold.otf +0 -0
- package/src/assets/fonts/Brockmann-BoldItalic.otf +0 -0
- package/src/assets/fonts/Brockmann-Medium.otf +0 -0
- package/src/assets/fonts/Brockmann-MediumItalic.otf +0 -0
- package/src/assets/fonts/Brockmann-Regular.otf +0 -0
- package/src/assets/fonts/Brockmann-RegularItalic.otf +0 -0
- package/src/assets/fonts/Brockmann-SemiBold.otf +0 -0
- package/src/assets/fonts/Brockmann-SemiBoldItalic.otf +0 -0
- package/src/assets/fonts/Brockmann_desktop_license.pdf +0 -0
- package/src/assets/fonts/brockmann-medium-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-regular-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-semibold-webfont.woff2 +0 -0
- package/src/components/action-list-item/ds-mobile-action-list-item.ts +83 -0
- package/src/components/action-list-item/index.ts +2 -0
- package/src/components/app-layout/ds-mobile-app-layout.css +343 -0
- package/src/components/app-layout/ds-mobile-app-layout.ts +271 -0
- package/src/components/app-layout/index.ts +2 -0
- package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +130 -0
- package/src/components/avatar-with-badge/index.ts +2 -0
- package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +273 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +110 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +167 -0
- package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +656 -0
- package/src/components/bottom-sheet/index.ts +3 -0
- package/src/components/comment/ds-mobile-comment.ts +516 -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 +158 -0
- package/src/components/content/index.ts +2 -0
- package/src/components/ds-mobile-tabs.css +372 -0
- package/src/components/ds-mobile-tabs.ts +217 -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.service.ts +98 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +514 -0
- package/src/components/handbook-detail-modal/index.ts +3 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +130 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +444 -0
- package/src/components/handbook-folder/index.ts +4 -0
- package/src/components/header-content/ds-mobile-header-content.ts +211 -0
- package/src/components/header-content/index.ts +2 -0
- package/src/components/index.ts +45 -0
- package/src/components/inline-photo/ds-mobile-inline-photo.ts +269 -0
- package/src/components/inline-photo/index.ts +1 -0
- package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.css +60 -0
- package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +280 -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 +197 -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.css +70 -0
- package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +594 -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 +331 -0
- package/src/components/lightbox/ds-mobile-lightbox-header.ts +173 -0
- package/src/components/lightbox/ds-mobile-lightbox-image.ts +464 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.css +375 -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 +293 -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 +499 -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/logo/ds-logo.ts +85 -0
- package/src/components/logo/index.ts +2 -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/page-details/ds-mobile-page-details.css +285 -0
- package/src/components/page-details/ds-mobile-page-details.ts +128 -0
- package/src/components/page-details/index.ts +2 -0
- package/src/components/page-main/ds-mobile-page-main.css +346 -0
- package/src/components/page-main/ds-mobile-page-main.ts +331 -0
- package/src/components/page-main/index.ts +2 -0
- package/src/components/post-card/ds-mobile-post-card.ts +685 -0
- package/src/components/post-card/ds-mobile-post-pdf-attachment.ts +124 -0
- package/src/components/post-card/index.ts +11 -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.service.ts +104 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +1273 -0
- package/src/components/post-detail-modal/index.ts +9 -0
- package/src/components/shared/directives/index.ts +2 -0
- package/src/components/shared/directives/long-press.directive.ts +208 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/shared/mobile-common.css +94 -0
- package/src/components/shared/mobile-page-base.css +315 -0
- package/src/components/shared/mobile-page-base.ts +70 -0
- package/src/components/swiper/ds-mobile-swiper.ts +123 -0
- package/src/components/swiper/index.ts +2 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.ts +132 -0
- package/src/components/tab-bar/index.ts +2 -0
- package/src/components/tabs/ds-mobile-tabs.css +405 -0
- package/src/components/tabs/ds-mobile-tabs.ts +204 -0
- package/src/components/tabs/index.ts +2 -0
- package/src/pages/community.page.ts +768 -0
- package/src/pages/handbook.page.ts +298 -0
- package/src/pages/home.page.ts +192 -0
- package/src/pages/index.ts +9 -0
- package/src/pages/inquiries.example.ts +212 -0
- package/src/pages/inquiry-detail.example.css +434 -0
- package/src/pages/inquiry-detail.example.ts +416 -0
- package/src/pages/mobile-tabs-example.component.ts +146 -0
- package/src/pages/post-create.page.ts +311 -0
- package/src/pages/post-detail.page.ts +295 -0
- package/src/pages/whitelabel-demo.page.ts +548 -0
- package/src/public-api.ts +5 -0
- package/src/services/user.service.ts +35 -0
- package/src/services/whitelabel.service.ts +171 -0
- package/src/styles/ionic.css +673 -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 -8294
- package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
- package/index.d.ts +0 -2860
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
signal,
|
|
4
|
+
computed,
|
|
5
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
6
|
+
Input,
|
|
7
|
+
ViewChild,
|
|
8
|
+
ElementRef,
|
|
9
|
+
AfterViewInit,
|
|
10
|
+
OnDestroy
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import { CommonModule } from '@angular/common';
|
|
13
|
+
import { FormsModule } from '@angular/forms';
|
|
14
|
+
import {
|
|
15
|
+
IonContent,
|
|
16
|
+
ModalController
|
|
17
|
+
} from '@ionic/angular/standalone';
|
|
18
|
+
import { Keyboard } from '@capacitor/keyboard';
|
|
19
|
+
import { DsIconButtonComponent } from '@propbinder/design-system';
|
|
20
|
+
import { DsIconComponent } from '@propbinder/design-system';
|
|
21
|
+
import { DsAvatarComponent } from '@propbinder/design-system';
|
|
22
|
+
import {
|
|
23
|
+
DsMobilePostCardComponent,
|
|
24
|
+
PostContentComponent,
|
|
25
|
+
PostTextComponent,
|
|
26
|
+
PostMediaComponent,
|
|
27
|
+
PostActionsComponent,
|
|
28
|
+
ActionLikeComponent,
|
|
29
|
+
ActionCommentComponent
|
|
30
|
+
} from '../post-card/ds-mobile-post-card';
|
|
31
|
+
import { DsMobileCommentComponent } from '../comment/ds-mobile-comment';
|
|
32
|
+
import { DsMobileLightboxService, LightboxAuthor } from '../lightbox';
|
|
33
|
+
import {
|
|
34
|
+
DsMobileBottomSheetService,
|
|
35
|
+
DsMobileCommentActionsBottomSheetComponent,
|
|
36
|
+
CommentActionResult
|
|
37
|
+
} from '../bottom-sheet';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Post data interface for the modal
|
|
41
|
+
*/
|
|
42
|
+
export interface PostDetailData {
|
|
43
|
+
postId: string;
|
|
44
|
+
authorName: string;
|
|
45
|
+
authorRole: string;
|
|
46
|
+
timestamp: string;
|
|
47
|
+
avatarInitials?: string;
|
|
48
|
+
avatarType?: 'photo' | 'initials';
|
|
49
|
+
avatarSrc?: string;
|
|
50
|
+
content: string;
|
|
51
|
+
imageSrc?: string;
|
|
52
|
+
imageAlt?: string;
|
|
53
|
+
isLiked?: boolean;
|
|
54
|
+
likeCount?: number;
|
|
55
|
+
commentCount?: number;
|
|
56
|
+
comments?: CommentData[];
|
|
57
|
+
focusComment?: boolean; // Auto-focus comment input when modal opens
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CommentData {
|
|
61
|
+
authorName: string;
|
|
62
|
+
authorRole: string;
|
|
63
|
+
timestamp: string;
|
|
64
|
+
avatarInitials: string;
|
|
65
|
+
content: string;
|
|
66
|
+
isLiked?: boolean;
|
|
67
|
+
likeCount?: number;
|
|
68
|
+
isOwnComment?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* DsMobilePostDetailModalComponent
|
|
73
|
+
*
|
|
74
|
+
* Modal wrapper for displaying post details with comments.
|
|
75
|
+
* Follows the same pattern as the lightbox modal for consistent behavior.
|
|
76
|
+
*
|
|
77
|
+
* Features:
|
|
78
|
+
* - Full post content display
|
|
79
|
+
* - Comments section
|
|
80
|
+
* - Image lightbox integration
|
|
81
|
+
* - Native modal controls (close, swipe down)
|
|
82
|
+
* - Safe area support
|
|
83
|
+
*
|
|
84
|
+
* This component is typically not used directly - use DsMobilePostDetailModalService instead.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* // Don't instantiate directly - use the service:
|
|
89
|
+
* constructor(private postModal: DsMobilePostDetailModalService) {}
|
|
90
|
+
*
|
|
91
|
+
* openPost() {
|
|
92
|
+
* this.postModal.open({
|
|
93
|
+
* postId: '123',
|
|
94
|
+
* authorName: 'John Doe',
|
|
95
|
+
* content: 'Post content...'
|
|
96
|
+
* });
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
@Component({
|
|
101
|
+
selector: 'ds-mobile-post-detail-modal',
|
|
102
|
+
standalone: true,
|
|
103
|
+
imports: [
|
|
104
|
+
CommonModule,
|
|
105
|
+
FormsModule,
|
|
106
|
+
IonContent,
|
|
107
|
+
DsIconButtonComponent,
|
|
108
|
+
DsIconComponent,
|
|
109
|
+
DsAvatarComponent,
|
|
110
|
+
PostTextComponent,
|
|
111
|
+
PostMediaComponent,
|
|
112
|
+
ActionLikeComponent,
|
|
113
|
+
ActionCommentComponent,
|
|
114
|
+
DsMobileCommentComponent
|
|
115
|
+
],
|
|
116
|
+
styleUrls: ['../shared/mobile-common.css'],
|
|
117
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
118
|
+
template: `
|
|
119
|
+
<ion-content [fullscreen]="true" [scrollY]="true" class="post-modal-content">
|
|
120
|
+
<div class="post-modal-wrapper">
|
|
121
|
+
<!-- Header with post author info -->
|
|
122
|
+
<div class="post-modal-header">
|
|
123
|
+
<div class="header-content">
|
|
124
|
+
<!-- Post author info -->
|
|
125
|
+
<div class="post-author-info">
|
|
126
|
+
<ds-avatar
|
|
127
|
+
[initials]="post().avatarInitials || ''"
|
|
128
|
+
[type]="post().avatarType || 'initials'"
|
|
129
|
+
[src]="post().avatarSrc || ''"
|
|
130
|
+
size="md"
|
|
131
|
+
/>
|
|
132
|
+
<div class="author-details">
|
|
133
|
+
<div class="author-name">{{ post().authorName }}</div>
|
|
134
|
+
<div class="author-meta">
|
|
135
|
+
<span>{{ post().authorRole }}</span>
|
|
136
|
+
<span class="separator">·</span>
|
|
137
|
+
<span>{{ post().timestamp }}</span>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Close button -->
|
|
143
|
+
<ds-icon-button
|
|
144
|
+
icon="remixCloseLine"
|
|
145
|
+
variant="secondary"
|
|
146
|
+
size="lg"
|
|
147
|
+
(click)="close()"
|
|
148
|
+
class="close-button"
|
|
149
|
+
aria-label="Luk opslag">
|
|
150
|
+
</ds-icon-button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- Post content -->
|
|
155
|
+
<div class="post-detail-container">
|
|
156
|
+
<!-- Post Section -->
|
|
157
|
+
<div class="post-section">
|
|
158
|
+
<div class="post-content-only">
|
|
159
|
+
<post-text>{{ post().content }}</post-text>
|
|
160
|
+
@if (post().imageSrc) {
|
|
161
|
+
<post-media>
|
|
162
|
+
<img
|
|
163
|
+
[src]="post().imageSrc"
|
|
164
|
+
[alt]="post().imageAlt || 'Post image'"
|
|
165
|
+
class="clickable-image"
|
|
166
|
+
(click)="openImageLightbox()"
|
|
167
|
+
/>
|
|
168
|
+
</post-media>
|
|
169
|
+
}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- Post actions -->
|
|
173
|
+
<div class="post-actions">
|
|
174
|
+
<action-like
|
|
175
|
+
[active]="post().isLiked || false"
|
|
176
|
+
[count]="post().likeCount || 0" />
|
|
177
|
+
<action-comment
|
|
178
|
+
[count]="post().commentCount || 0"
|
|
179
|
+
(commentClick)="focusCommentInput()" />
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<!-- Comments Section -->
|
|
184
|
+
<div class="comments-section">
|
|
185
|
+
@if (post().comments && post().comments!.length > 0) {
|
|
186
|
+
<h2 class="comments-header">{{ post().comments!.length }} {{ post().comments!.length === 1 ? 'reply' : 'replies' }}</h2>
|
|
187
|
+
|
|
188
|
+
<div class="comments-list">
|
|
189
|
+
@for (comment of post().comments!; track comment.authorName + comment.timestamp) {
|
|
190
|
+
<ds-mobile-comment
|
|
191
|
+
[authorName]="comment.authorName"
|
|
192
|
+
[authorRole]="comment.authorRole"
|
|
193
|
+
[timestamp]="comment.timestamp"
|
|
194
|
+
[avatarInitials]="comment.avatarInitials"
|
|
195
|
+
[content]="comment.content"
|
|
196
|
+
[isLiked]="comment.isLiked || false"
|
|
197
|
+
[likeCount]="comment.likeCount || 0"
|
|
198
|
+
[clickable]="true"
|
|
199
|
+
[isOwnComment]="comment.isOwnComment || false"
|
|
200
|
+
(replyClick)="handleReply(comment.authorName, comment.content)"
|
|
201
|
+
(editClick)="handleEditComment(comment.authorName, comment.content, comment.timestamp)"
|
|
202
|
+
(longPress)="handleCommentLongPress(comment.authorName, comment.content, comment.isOwnComment || false)" />
|
|
203
|
+
}
|
|
204
|
+
</div>
|
|
205
|
+
} @else {
|
|
206
|
+
<!-- Empty State -->
|
|
207
|
+
<div class="comments-empty-state">
|
|
208
|
+
<img
|
|
209
|
+
src="/Assets/Empty state-chat.png"
|
|
210
|
+
alt="Ingen kommentarer endnu"
|
|
211
|
+
class="empty-state-image"
|
|
212
|
+
/>
|
|
213
|
+
<h3 class="empty-state-title">Ingen svar endnu</h3>
|
|
214
|
+
<p class="empty-state-description">Vær den første til at svare på dette opslag</p>
|
|
215
|
+
</div>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
<!-- Bottom spacer for fixed composer -->
|
|
219
|
+
<div class="composer-spacer"></div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</ion-content>
|
|
224
|
+
|
|
225
|
+
<!-- Fixed comment composer -->
|
|
226
|
+
<div class="comment-composer-fixed">
|
|
227
|
+
<div class="comment-composer">
|
|
228
|
+
<!-- Edit indicator -->
|
|
229
|
+
@if (editingComment()) {
|
|
230
|
+
<div class="edit-indicator">
|
|
231
|
+
<div class="edit-indicator-content">
|
|
232
|
+
<ds-icon name="remixEditLine" size="16px" />
|
|
233
|
+
<span class="edit-text">Redigerer kommentar</span>
|
|
234
|
+
</div>
|
|
235
|
+
<button class="cancel-edit" (click)="cancelEdit()">
|
|
236
|
+
<ds-icon name="remixCloseLine" size="16px" />
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
} @else if (replyingTo()) {
|
|
240
|
+
<!-- Reply indicator -->
|
|
241
|
+
<div class="reply-indicator">
|
|
242
|
+
<div class="reply-indicator-content">
|
|
243
|
+
<ds-icon name="remixReplyLine" size="16px" />
|
|
244
|
+
<span class="reply-to-text">
|
|
245
|
+
Svarer til <span class="reply-author">{{ replyingTo()!.authorName }}</span>
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
<button class="cancel-reply" (click)="cancelReply()">
|
|
249
|
+
<ds-icon name="remixCloseLine" size="16px" />
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
<div class="composer-content">
|
|
255
|
+
<ds-avatar
|
|
256
|
+
[initials]="currentUserInitials()"
|
|
257
|
+
[type]="'initials'"
|
|
258
|
+
size="md"
|
|
259
|
+
/>
|
|
260
|
+
<div class="composer-input-wrapper">
|
|
261
|
+
<!-- Mention menu -->
|
|
262
|
+
@if (showMentionMenu() && filteredUsers().length > 0 && !editingComment()) {
|
|
263
|
+
<div class="mention-menu">
|
|
264
|
+
@for (user of filteredUsers(); track user.name) {
|
|
265
|
+
<button
|
|
266
|
+
class="mention-menu-item"
|
|
267
|
+
(click)="selectMention(user.name)">
|
|
268
|
+
<ds-avatar
|
|
269
|
+
[initials]="user.initials"
|
|
270
|
+
[type]="'initials'"
|
|
271
|
+
size="sm" />
|
|
272
|
+
<div class="mention-user-info">
|
|
273
|
+
<span class="mention-user-name">{{ user.name }}</span>
|
|
274
|
+
<span class="mention-user-role">{{ user.role }}</span>
|
|
275
|
+
</div>
|
|
276
|
+
</button>
|
|
277
|
+
}
|
|
278
|
+
</div>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
<textarea
|
|
282
|
+
#commentInput
|
|
283
|
+
class="composer-input"
|
|
284
|
+
[placeholder]="editingComment() ? 'Rediger din kommentar...' : (replyingTo() ? 'Tilføj et svar...' : 'Tilføj et svar...')"
|
|
285
|
+
[(ngModel)]="commentText"
|
|
286
|
+
(input)="handleInput($event)"
|
|
287
|
+
(focus)="showKeyboard()"
|
|
288
|
+
(click)="showKeyboard()"
|
|
289
|
+
rows="1"
|
|
290
|
+
></textarea>
|
|
291
|
+
</div>
|
|
292
|
+
@if (commentText().trim().length > 0) {
|
|
293
|
+
<ds-icon-button
|
|
294
|
+
icon="remixCheckLine"
|
|
295
|
+
variant="primary"
|
|
296
|
+
size="sm"
|
|
297
|
+
(clicked)="submitComment()"
|
|
298
|
+
aria-label="Send kommentar"
|
|
299
|
+
class="send-button-fixed">
|
|
300
|
+
</ds-icon-button>
|
|
301
|
+
}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
`,
|
|
306
|
+
styles: [`
|
|
307
|
+
:host {
|
|
308
|
+
display: block;
|
|
309
|
+
position: relative;
|
|
310
|
+
height: 100%;
|
|
311
|
+
width: 100%;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.post-modal-content {
|
|
315
|
+
--background: var(--color-background-neutral-primary, #ffffff);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.post-modal-wrapper {
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
min-height: 100%;
|
|
322
|
+
min-height: 100dvh; /* Use dynamic viewport height for proper iOS safe area handling */
|
|
323
|
+
background: var(--color-background-neutral-primary, #ffffff);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.post-modal-header {
|
|
327
|
+
position: sticky;
|
|
328
|
+
top: 0;
|
|
329
|
+
z-index: 10;
|
|
330
|
+
background: var(--color-background-neutral-primary, #ffffff);
|
|
331
|
+
border-bottom: 1px solid var(--border-color-default);
|
|
332
|
+
padding: 0 16px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.header-content {
|
|
336
|
+
display: flex;
|
|
337
|
+
align-items: center;
|
|
338
|
+
justify-content: space-between;
|
|
339
|
+
gap: 12px;
|
|
340
|
+
min-height: 72px;
|
|
341
|
+
/* No padding needed - StatusBar.setOverlaysWebView(false) handles all spacing */
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.post-author-info {
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: 12px;
|
|
348
|
+
flex: 1;
|
|
349
|
+
min-width: 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.author-details {
|
|
353
|
+
display: flex;
|
|
354
|
+
flex-direction: column;
|
|
355
|
+
min-width: 0;
|
|
356
|
+
flex: 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Author name and meta styles imported from mobile-common.css */
|
|
360
|
+
|
|
361
|
+
.author-meta .separator {
|
|
362
|
+
color: var(--color-text-tertiary, #a0a0a0);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.close-button {
|
|
366
|
+
flex-shrink: 0;
|
|
367
|
+
border-radius: 50%;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.close-button::ng-deep button {
|
|
371
|
+
border-radius: 50% !important;
|
|
372
|
+
width: 36px !important;
|
|
373
|
+
height: 36px !important;
|
|
374
|
+
min-width: 36px !important;
|
|
375
|
+
min-height: 36px !important;
|
|
376
|
+
padding: 0 !important;
|
|
377
|
+
display: flex !important;
|
|
378
|
+
align-items: center !important;
|
|
379
|
+
justify-content: center !important;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.post-detail-container {
|
|
383
|
+
display: flex;
|
|
384
|
+
flex-direction: column;
|
|
385
|
+
gap: 16px;
|
|
386
|
+
width: 100%;
|
|
387
|
+
max-width: 640px;
|
|
388
|
+
margin: 0 auto;
|
|
389
|
+
padding: 16px 0 20px 0;
|
|
390
|
+
flex: 1;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.post-section {
|
|
394
|
+
width: 100%;
|
|
395
|
+
border-bottom: 1px solid var(--border-color-default);
|
|
396
|
+
padding: 0 0 16px 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.post-content-only {
|
|
400
|
+
font-size: var(--font-size-sm);
|
|
401
|
+
line-height: 24px;
|
|
402
|
+
color: var(--color-text-primary, #1a1a1a);
|
|
403
|
+
margin-bottom: 16px;
|
|
404
|
+
padding: 0 20px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.post-content-only post-media {
|
|
408
|
+
margin-top: 16px;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.post-actions {
|
|
412
|
+
display: flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
gap: 16px;
|
|
415
|
+
padding: 0 20px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.clickable-image {
|
|
419
|
+
cursor: pointer;
|
|
420
|
+
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
421
|
+
border-radius: 8px;
|
|
422
|
+
display: block;
|
|
423
|
+
width: 100%;
|
|
424
|
+
aspect-ratio: 16/9;
|
|
425
|
+
object-fit: cover;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.clickable-image:active {
|
|
429
|
+
transform: scale(0.98);
|
|
430
|
+
opacity: 0.9;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.comments-section {
|
|
434
|
+
display: flex;
|
|
435
|
+
flex-direction: column;
|
|
436
|
+
margin-left: 0;
|
|
437
|
+
margin-right: 0;
|
|
438
|
+
padding: 0 20px;
|
|
439
|
+
padding-bottom: 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.comments-header {
|
|
443
|
+
font-family: 'Brockmann', sans-serif;
|
|
444
|
+
font-size: var(--font-size-base);
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
line-height: 24px;
|
|
447
|
+
color: var(--color-text-primary, #1a1a1a);
|
|
448
|
+
margin: 0 0 16px 0;
|
|
449
|
+
padding-left: 0;
|
|
450
|
+
padding-right: 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.comments-list {
|
|
454
|
+
display: flex;
|
|
455
|
+
flex-direction: column;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* Empty State */
|
|
459
|
+
.comments-empty-state {
|
|
460
|
+
display: flex;
|
|
461
|
+
flex-direction: column;
|
|
462
|
+
align-items: center;
|
|
463
|
+
justify-content: center;
|
|
464
|
+
padding: 60px 20px;
|
|
465
|
+
text-align: center;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.empty-state-image {
|
|
469
|
+
width: 96px;
|
|
470
|
+
height: 96px;
|
|
471
|
+
margin-bottom: 24px;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.empty-state-title {
|
|
475
|
+
font-family: 'Brockmann', sans-serif;
|
|
476
|
+
font-size: var(--font-size-base);
|
|
477
|
+
font-weight: 600;
|
|
478
|
+
line-height: 1.3;
|
|
479
|
+
color: var(--color-text-primary, #1a1a1a);
|
|
480
|
+
margin: 0 0 8px 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.empty-state-description {
|
|
484
|
+
font-family: 'Brockmann', sans-serif;
|
|
485
|
+
font-size: var(--font-size-sm);
|
|
486
|
+
font-weight: 400;
|
|
487
|
+
line-height: 1.4;
|
|
488
|
+
color: var(--color-text-secondary, #737373);
|
|
489
|
+
margin: 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.composer-spacer {
|
|
493
|
+
/* Match full composer height:
|
|
494
|
+
- Border: 1px
|
|
495
|
+
- Top padding: 12px
|
|
496
|
+
- Composer content: ~56px (avatar + input wrapper)
|
|
497
|
+
- Bottom padding: 12px + safe area
|
|
498
|
+
Total: ~81px + safe area */
|
|
499
|
+
height: calc(81px + env(safe-area-inset-bottom, 0px));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.bottom-spacer {
|
|
503
|
+
height: 0px;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/* Fixed Comment Composer Container */
|
|
507
|
+
.comment-composer-fixed {
|
|
508
|
+
position: fixed;
|
|
509
|
+
bottom: 48px; /* Compensate for modal's margin-top: 48px */
|
|
510
|
+
left: 0;
|
|
511
|
+
right: 0;
|
|
512
|
+
z-index: 1000;
|
|
513
|
+
pointer-events: none;
|
|
514
|
+
/* Slide up with keyboard on native apps */
|
|
515
|
+
transform: translateY(calc(-1 * var(--keyboard-height, 0px)));
|
|
516
|
+
transition: transform 0.3s ease-out;
|
|
517
|
+
/* Ensure it's within the modal viewport */
|
|
518
|
+
max-width: 100vw;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* Comment Composer */
|
|
522
|
+
.comment-composer {
|
|
523
|
+
pointer-events: auto;
|
|
524
|
+
background: var(--color-background-neutral-primary, #ffffff);
|
|
525
|
+
border-top: 1px solid var(--border-color-default);
|
|
526
|
+
padding: 12px 16px;
|
|
527
|
+
/* Use dynamic viewport height safe area - matches tabs fix */
|
|
528
|
+
/* For web browsers: 12px default; for native iOS: env(safe-area-inset-bottom) */
|
|
529
|
+
padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
|
|
530
|
+
width: 100%;
|
|
531
|
+
display: flex;
|
|
532
|
+
flex-direction: column;
|
|
533
|
+
gap: 8px;
|
|
534
|
+
/* White box shadow to cover content gap between keyboard and composer */
|
|
535
|
+
box-shadow: 100px 150px 0 150px var(--color-background-neutral-primary, #ffffff);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* Edit indicator */
|
|
539
|
+
.edit-indicator {
|
|
540
|
+
display: flex;
|
|
541
|
+
align-items: center;
|
|
542
|
+
justify-content: space-between;
|
|
543
|
+
padding: 8px 12px;
|
|
544
|
+
background: var(--color-background-brand-subtle, #f0edfe);
|
|
545
|
+
border-radius: 8px;
|
|
546
|
+
animation: slideDown 0.2s ease-out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.edit-indicator-content {
|
|
550
|
+
display: flex;
|
|
551
|
+
align-items: center;
|
|
552
|
+
gap: 8px;
|
|
553
|
+
color: var(--color-brand-base, #6B5FF5);
|
|
554
|
+
flex: 1;
|
|
555
|
+
min-width: 0;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.edit-text {
|
|
559
|
+
font-family: 'Brockmann', sans-serif;
|
|
560
|
+
font-size: var(--font-size-sm);
|
|
561
|
+
font-weight: 500;
|
|
562
|
+
line-height: 18px;
|
|
563
|
+
color: var(--color-brand-base, #6B5FF5);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.cancel-edit {
|
|
567
|
+
background: none;
|
|
568
|
+
border: none;
|
|
569
|
+
padding: 4px;
|
|
570
|
+
cursor: pointer;
|
|
571
|
+
display: flex;
|
|
572
|
+
align-items: center;
|
|
573
|
+
justify-content: center;
|
|
574
|
+
color: var(--color-brand-base, #6B5FF5);
|
|
575
|
+
border-radius: 4px;
|
|
576
|
+
transition: background 0.2s ease;
|
|
577
|
+
flex-shrink: 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.cancel-edit:active {
|
|
581
|
+
background: var(--color-brand-subtle, #e0dbfe);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* Reply indicator */
|
|
585
|
+
.reply-indicator {
|
|
586
|
+
display: flex;
|
|
587
|
+
align-items: center;
|
|
588
|
+
justify-content: space-between;
|
|
589
|
+
padding: 8px 12px;
|
|
590
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
591
|
+
border-radius: 8px;
|
|
592
|
+
animation: slideDown 0.2s ease-out;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.reply-indicator-content {
|
|
596
|
+
display: flex;
|
|
597
|
+
align-items: center;
|
|
598
|
+
gap: 4px;
|
|
599
|
+
color: var(--color-text-secondary, #737373);
|
|
600
|
+
flex: 1;
|
|
601
|
+
min-width: 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.reply-to-text {
|
|
605
|
+
font-family: 'Brockmann', sans-serif;
|
|
606
|
+
font-size: var(--font-size-sm);
|
|
607
|
+
line-height: 18px;
|
|
608
|
+
color: var(--color-text-secondary, #737373);
|
|
609
|
+
white-space: nowrap;
|
|
610
|
+
overflow: hidden;
|
|
611
|
+
text-overflow: ellipsis;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.reply-author {
|
|
615
|
+
color: var(--color-brand-base, #6B5FF5);
|
|
616
|
+
font-weight: 600;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.cancel-reply {
|
|
620
|
+
background: none;
|
|
621
|
+
border: none;
|
|
622
|
+
padding: 4px;
|
|
623
|
+
cursor: pointer;
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: center;
|
|
626
|
+
justify-content: center;
|
|
627
|
+
color: var(--color-text-secondary, #737373);
|
|
628
|
+
border-radius: 4px;
|
|
629
|
+
transition: background 0.2s ease;
|
|
630
|
+
flex-shrink: 0;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.cancel-reply:active {
|
|
634
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
@keyframes slideDown {
|
|
638
|
+
from {
|
|
639
|
+
opacity: 0;
|
|
640
|
+
transform: translateY(-10px);
|
|
641
|
+
}
|
|
642
|
+
to {
|
|
643
|
+
opacity: 1;
|
|
644
|
+
transform: translateY(0);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.composer-content {
|
|
649
|
+
display: flex;
|
|
650
|
+
align-items: flex-start;
|
|
651
|
+
gap: 12px;
|
|
652
|
+
width: 100%;
|
|
653
|
+
position: relative;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.composer-content ds-avatar {
|
|
657
|
+
position: relative;
|
|
658
|
+
top: 6px;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.composer-input-wrapper {
|
|
662
|
+
flex: 1;
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: flex-start;
|
|
665
|
+
gap: 8px;
|
|
666
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
667
|
+
border-radius: 24px;
|
|
668
|
+
padding: 12px 16px;
|
|
669
|
+
padding-right: 48px; /* Extra padding for fixed send button */
|
|
670
|
+
min-height: 44px;
|
|
671
|
+
position: relative;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* Mention menu */
|
|
675
|
+
.mention-menu {
|
|
676
|
+
position: absolute;
|
|
677
|
+
bottom: 100%;
|
|
678
|
+
left: 0;
|
|
679
|
+
right: 0;
|
|
680
|
+
background: var(--color-background-neutral-primary, #ffffff);
|
|
681
|
+
border-radius: 12px;
|
|
682
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
683
|
+
margin-bottom: 8px;
|
|
684
|
+
max-height: 200px;
|
|
685
|
+
overflow-y: auto;
|
|
686
|
+
z-index: 10;
|
|
687
|
+
animation: slideUp 0.2s ease-out;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@keyframes slideUp {
|
|
691
|
+
from {
|
|
692
|
+
opacity: 0;
|
|
693
|
+
transform: translateY(10px);
|
|
694
|
+
}
|
|
695
|
+
to {
|
|
696
|
+
opacity: 1;
|
|
697
|
+
transform: translateY(0);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.mention-menu-item {
|
|
702
|
+
display: flex;
|
|
703
|
+
align-items: center;
|
|
704
|
+
gap: 12px;
|
|
705
|
+
padding: 12px;
|
|
706
|
+
border: none;
|
|
707
|
+
background: none;
|
|
708
|
+
width: 100%;
|
|
709
|
+
text-align: left;
|
|
710
|
+
cursor: pointer;
|
|
711
|
+
transition: background 0.2s ease;
|
|
712
|
+
border-bottom: 1px solid var(--border-color-default);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.mention-menu-item:last-child {
|
|
716
|
+
border-bottom: none;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.mention-menu-item:active {
|
|
720
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.mention-user-info {
|
|
724
|
+
display: flex;
|
|
725
|
+
align-items: center;
|
|
726
|
+
gap: 8px;
|
|
727
|
+
flex: 1;
|
|
728
|
+
min-width: 0;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.mention-user-name {
|
|
732
|
+
font-family: 'Brockmann', sans-serif;
|
|
733
|
+
font-size: var(--font-size-base);
|
|
734
|
+
font-weight: 600;
|
|
735
|
+
line-height: 20px;
|
|
736
|
+
color: var(--color-text-primary, #1a1a1a);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.mention-user-role {
|
|
740
|
+
font-family: 'Brockmann', sans-serif;
|
|
741
|
+
font-size: var(--font-size-sm);
|
|
742
|
+
line-height: 18px;
|
|
743
|
+
color: var(--color-text-secondary, #737373);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.composer-input {
|
|
747
|
+
flex: 1;
|
|
748
|
+
border: none;
|
|
749
|
+
background: transparent;
|
|
750
|
+
font-family: 'Brockmann', sans-serif;
|
|
751
|
+
font-size: var(--font-size-sm);
|
|
752
|
+
line-height: 20px;
|
|
753
|
+
color: var(--color-text-primary, #1a1a1a);
|
|
754
|
+
outline: none;
|
|
755
|
+
resize: none;
|
|
756
|
+
min-height: 20px;
|
|
757
|
+
max-height: 120px;
|
|
758
|
+
overflow-y: auto;
|
|
759
|
+
padding: 0;
|
|
760
|
+
margin: 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.composer-input::placeholder {
|
|
764
|
+
color: var(--color-text-tertiary, #a0a0a0);
|
|
765
|
+
font-size: var(--font-size-sm);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/* Style the send button (ds-icon-button) - positioned in top right corner */
|
|
769
|
+
.send-button-fixed {
|
|
770
|
+
position: absolute;
|
|
771
|
+
top: 6px;
|
|
772
|
+
right: 6px;
|
|
773
|
+
z-index: 10;
|
|
774
|
+
flex-shrink: 0;
|
|
775
|
+
animation: slideInFromRight 0.2s ease-out;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.send-button-fixed::ng-deep button {
|
|
779
|
+
width: 32px !important;
|
|
780
|
+
height: 32px !important;
|
|
781
|
+
min-width: 32px !important;
|
|
782
|
+
min-height: 32px !important;
|
|
783
|
+
padding: 0 !important;
|
|
784
|
+
border-radius: 50% !important;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/* Keep old style for reference but won't be used */
|
|
788
|
+
.composer-input-wrapper ds-icon-button {
|
|
789
|
+
flex-shrink: 0;
|
|
790
|
+
animation: slideInFromRight 0.2s ease-out;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.composer-input-wrapper ds-icon-button::ng-deep button {
|
|
794
|
+
width: 32px !important;
|
|
795
|
+
height: 32px !important;
|
|
796
|
+
min-width: 32px !important;
|
|
797
|
+
min-height: 32px !important;
|
|
798
|
+
padding: 0 !important;
|
|
799
|
+
border-radius: 50% !important;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* Slide in animation from right */
|
|
803
|
+
@keyframes slideInFromRight {
|
|
804
|
+
from {
|
|
805
|
+
opacity: 0;
|
|
806
|
+
transform: translateX(20px) scale(0.8);
|
|
807
|
+
}
|
|
808
|
+
to {
|
|
809
|
+
opacity: 1;
|
|
810
|
+
transform: translateX(0) scale(1);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/* Safe area support */
|
|
815
|
+
@supports (padding: env(safe-area-inset-bottom)) {
|
|
816
|
+
.post-detail-container {
|
|
817
|
+
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
`]
|
|
821
|
+
})
|
|
822
|
+
export class DsMobilePostDetailModalComponent implements AfterViewInit, OnDestroy {
|
|
823
|
+
// Post data passed from service
|
|
824
|
+
@Input() postData!: PostDetailData;
|
|
825
|
+
|
|
826
|
+
// ViewChild for comment input
|
|
827
|
+
@ViewChild('commentInput') commentInput?: ElementRef<HTMLTextAreaElement>;
|
|
828
|
+
|
|
829
|
+
// Signal for reactive post data
|
|
830
|
+
post = signal<PostDetailData>({
|
|
831
|
+
postId: '',
|
|
832
|
+
authorName: '',
|
|
833
|
+
authorRole: '',
|
|
834
|
+
timestamp: '',
|
|
835
|
+
content: '',
|
|
836
|
+
comments: []
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Comment composer state
|
|
840
|
+
commentText = signal('');
|
|
841
|
+
currentUserInitials = signal('LM');
|
|
842
|
+
replyingTo = signal<{ authorName: string; content: string } | null>(null);
|
|
843
|
+
editingComment = signal<{ authorName: string; originalContent: string; timestamp: string } | null>(null);
|
|
844
|
+
|
|
845
|
+
// Mention menu state
|
|
846
|
+
showMentionMenu = signal(false);
|
|
847
|
+
mentionQuery = signal('');
|
|
848
|
+
|
|
849
|
+
// Get available users to mention (post author + commenters)
|
|
850
|
+
availableUsers = computed(() => {
|
|
851
|
+
const post = this.post();
|
|
852
|
+
const users: Array<{ name: string; initials: string; role: string }> = [];
|
|
853
|
+
|
|
854
|
+
// Add post author
|
|
855
|
+
users.push({
|
|
856
|
+
name: post.authorName,
|
|
857
|
+
initials: post.avatarInitials || post.authorName.split(' ').map(n => n[0]).join(''),
|
|
858
|
+
role: post.authorRole
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// Add unique commenters
|
|
862
|
+
const commenterNames = new Set<string>();
|
|
863
|
+
post.comments?.forEach(comment => {
|
|
864
|
+
if (!commenterNames.has(comment.authorName)) {
|
|
865
|
+
commenterNames.add(comment.authorName);
|
|
866
|
+
users.push({
|
|
867
|
+
name: comment.authorName,
|
|
868
|
+
initials: comment.avatarInitials,
|
|
869
|
+
role: comment.authorRole
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return users;
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Filtered users based on mention query
|
|
878
|
+
filteredUsers = computed(() => {
|
|
879
|
+
const query = this.mentionQuery().toLowerCase();
|
|
880
|
+
if (!query) return this.availableUsers();
|
|
881
|
+
return this.availableUsers().filter(user =>
|
|
882
|
+
user.name.toLowerCase().includes(query)
|
|
883
|
+
);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
constructor(
|
|
887
|
+
private modalController: ModalController,
|
|
888
|
+
private lightbox: DsMobileLightboxService,
|
|
889
|
+
private bottomSheet: DsMobileBottomSheetService
|
|
890
|
+
) {}
|
|
891
|
+
|
|
892
|
+
ngOnInit(): void {
|
|
893
|
+
// Initialize post data from input
|
|
894
|
+
if (this.postData) {
|
|
895
|
+
this.post.set(this.postData);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Set up keyboard listeners to update CSS variable for composer positioning
|
|
899
|
+
this.setupKeyboardListeners();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
ngAfterViewInit(): void {
|
|
903
|
+
// Auto-focus comment input if requested
|
|
904
|
+
if (this.postData?.focusComment) {
|
|
905
|
+
// Small delay to ensure modal animation is complete
|
|
906
|
+
setTimeout(() => {
|
|
907
|
+
this.commentInput?.nativeElement.focus();
|
|
908
|
+
// Show keyboard on mobile
|
|
909
|
+
this.showKeyboard();
|
|
910
|
+
}, 300);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
ngOnDestroy(): void {
|
|
915
|
+
// Clean up keyboard listeners when modal is destroyed
|
|
916
|
+
this.cleanupKeyboardListeners();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Set up keyboard event listeners to adjust composer position
|
|
921
|
+
* The CSS uses --keyboard-height variable to translate the composer up
|
|
922
|
+
*/
|
|
923
|
+
private setupKeyboardListeners(): void {
|
|
924
|
+
Keyboard.addListener('keyboardWillShow', (info) => {
|
|
925
|
+
document.documentElement.style.setProperty('--keyboard-height', `${info.keyboardHeight}px`);
|
|
926
|
+
}).catch(e => console.log('Keyboard listeners not available:', e));
|
|
927
|
+
|
|
928
|
+
Keyboard.addListener('keyboardWillHide', () => {
|
|
929
|
+
document.documentElement.style.setProperty('--keyboard-height', '0px');
|
|
930
|
+
}).catch(e => console.log('Keyboard listeners not available:', e));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Clean up keyboard event listeners
|
|
935
|
+
*/
|
|
936
|
+
private cleanupKeyboardListeners(): void {
|
|
937
|
+
Keyboard.removeAllListeners().catch(e => console.log('Keyboard cleanup not available:', e));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Show the keyboard when user interacts with input
|
|
942
|
+
*/
|
|
943
|
+
showKeyboard(): void {
|
|
944
|
+
Keyboard.show().catch(e => console.log('Keyboard.show() not available'));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Focus the comment input when comment icon is tapped
|
|
949
|
+
*/
|
|
950
|
+
focusCommentInput(): void {
|
|
951
|
+
// Focus the input
|
|
952
|
+
this.commentInput?.nativeElement.focus();
|
|
953
|
+
// Show keyboard on mobile
|
|
954
|
+
this.showKeyboard();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Handle input changes and detect @ mentions
|
|
959
|
+
*/
|
|
960
|
+
handleInput(event: Event): void {
|
|
961
|
+
const textarea = event.target as HTMLTextAreaElement;
|
|
962
|
+
const text = textarea.value;
|
|
963
|
+
const cursorPosition = textarea.selectionStart || 0;
|
|
964
|
+
|
|
965
|
+
// Auto-resize textarea
|
|
966
|
+
textarea.style.height = 'auto';
|
|
967
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
968
|
+
|
|
969
|
+
// Find the last @ before cursor
|
|
970
|
+
const textBeforeCursor = text.substring(0, cursorPosition);
|
|
971
|
+
const lastAtIndex = textBeforeCursor.lastIndexOf('@');
|
|
972
|
+
|
|
973
|
+
if (lastAtIndex !== -1) {
|
|
974
|
+
// Check if there's a space after @
|
|
975
|
+
const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
|
|
976
|
+
const hasSpace = textAfterAt.includes(' ');
|
|
977
|
+
|
|
978
|
+
if (!hasSpace) {
|
|
979
|
+
// Show mention menu
|
|
980
|
+
this.showMentionMenu.set(true);
|
|
981
|
+
this.mentionQuery.set(textAfterAt);
|
|
982
|
+
} else {
|
|
983
|
+
this.showMentionMenu.set(false);
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
this.showMentionMenu.set(false);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Select a user from mention menu - show as reply indicator instead of inline mention
|
|
992
|
+
*/
|
|
993
|
+
selectMention(userName: string): void {
|
|
994
|
+
// Set as reply (similar to clicking Reply on a comment)
|
|
995
|
+
this.replyingTo.set({ authorName: userName, content: '' });
|
|
996
|
+
|
|
997
|
+
// Clear the @ from the input
|
|
998
|
+
const currentText = this.commentText();
|
|
999
|
+
const textWithoutMention = currentText.substring(0, currentText.lastIndexOf('@'));
|
|
1000
|
+
this.commentText.set(textWithoutMention);
|
|
1001
|
+
|
|
1002
|
+
// Hide mention menu
|
|
1003
|
+
this.showMentionMenu.set(false);
|
|
1004
|
+
|
|
1005
|
+
// Focus back on input
|
|
1006
|
+
setTimeout(() => {
|
|
1007
|
+
this.commentInput?.nativeElement.focus();
|
|
1008
|
+
}, 0);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Handle reply to a comment
|
|
1013
|
+
*/
|
|
1014
|
+
handleReply(authorName: string, content: string): void {
|
|
1015
|
+
this.replyingTo.set({ authorName, content });
|
|
1016
|
+
// Focus the input and show keyboard
|
|
1017
|
+
setTimeout(() => {
|
|
1018
|
+
this.commentInput?.nativeElement.focus();
|
|
1019
|
+
this.showKeyboard();
|
|
1020
|
+
}, 100);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Cancel reply
|
|
1025
|
+
*/
|
|
1026
|
+
cancelReply(): void {
|
|
1027
|
+
this.replyingTo.set(null);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Cancel edit
|
|
1032
|
+
*/
|
|
1033
|
+
cancelEdit(): void {
|
|
1034
|
+
this.editingComment.set(null);
|
|
1035
|
+
this.commentText.set('');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Handle edit comment
|
|
1040
|
+
*/
|
|
1041
|
+
handleEditComment(authorName: string, originalContent: string, timestamp: string): void {
|
|
1042
|
+
// Clear reply state if active
|
|
1043
|
+
this.replyingTo.set(null);
|
|
1044
|
+
|
|
1045
|
+
// Remove @mention from the content if it exists
|
|
1046
|
+
let contentToEdit = originalContent;
|
|
1047
|
+
const mentionMatch = originalContent.match(/^@([A-Za-z]+(?:\s+[A-Za-z]+)?)\s+/);
|
|
1048
|
+
if (mentionMatch) {
|
|
1049
|
+
contentToEdit = originalContent.substring(mentionMatch[0].length);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Set edit state
|
|
1053
|
+
this.editingComment.set({ authorName, originalContent, timestamp });
|
|
1054
|
+
|
|
1055
|
+
// Populate the input with existing content
|
|
1056
|
+
this.commentText.set(contentToEdit);
|
|
1057
|
+
|
|
1058
|
+
// Focus the input, show keyboard, and auto-resize
|
|
1059
|
+
setTimeout(() => {
|
|
1060
|
+
if (this.commentInput?.nativeElement) {
|
|
1061
|
+
const textarea = this.commentInput.nativeElement;
|
|
1062
|
+
textarea.focus();
|
|
1063
|
+
|
|
1064
|
+
// Auto-resize textarea to fit content
|
|
1065
|
+
textarea.style.height = 'auto';
|
|
1066
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
1067
|
+
|
|
1068
|
+
this.showKeyboard();
|
|
1069
|
+
}
|
|
1070
|
+
}, 100);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Close the modal
|
|
1075
|
+
*/
|
|
1076
|
+
close(): void {
|
|
1077
|
+
this.modalController.dismiss();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Submit a comment
|
|
1082
|
+
*/
|
|
1083
|
+
submitComment(): void {
|
|
1084
|
+
const text = this.commentText().trim();
|
|
1085
|
+
if (!text) return;
|
|
1086
|
+
|
|
1087
|
+
const currentPost = this.post();
|
|
1088
|
+
|
|
1089
|
+
// Check if we're editing an existing comment
|
|
1090
|
+
if (this.editingComment()) {
|
|
1091
|
+
console.log('[PostDetailModal] Updating comment:', text);
|
|
1092
|
+
|
|
1093
|
+
const editing = this.editingComment()!;
|
|
1094
|
+
|
|
1095
|
+
// Update the existing comment
|
|
1096
|
+
const updatedComments = currentPost.comments?.map(comment => {
|
|
1097
|
+
if (comment.authorName === editing.authorName &&
|
|
1098
|
+
comment.content === editing.originalContent &&
|
|
1099
|
+
comment.timestamp === editing.timestamp) {
|
|
1100
|
+
return {
|
|
1101
|
+
...comment,
|
|
1102
|
+
content: text,
|
|
1103
|
+
timestamp: 'Just now (edited)'
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
return comment;
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
this.post.set({
|
|
1110
|
+
...currentPost,
|
|
1111
|
+
comments: updatedComments
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Clear edit state
|
|
1115
|
+
this.editingComment.set(null);
|
|
1116
|
+
} else {
|
|
1117
|
+
// Create new comment
|
|
1118
|
+
console.log('[PostDetailModal] Submitting comment:', text);
|
|
1119
|
+
|
|
1120
|
+
const newComment: CommentData = {
|
|
1121
|
+
authorName: 'Lars Mikkelsen',
|
|
1122
|
+
authorRole: 'You',
|
|
1123
|
+
timestamp: 'Just now',
|
|
1124
|
+
avatarInitials: this.currentUserInitials(),
|
|
1125
|
+
content: this.replyingTo()
|
|
1126
|
+
? `@${this.replyingTo()!.authorName} ${text}`
|
|
1127
|
+
: text,
|
|
1128
|
+
isLiked: false,
|
|
1129
|
+
likeCount: 0,
|
|
1130
|
+
isOwnComment: true
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// Add comment to the list
|
|
1134
|
+
const updatedComments = [...(currentPost.comments || []), newComment];
|
|
1135
|
+
|
|
1136
|
+
this.post.set({
|
|
1137
|
+
...currentPost,
|
|
1138
|
+
comments: updatedComments,
|
|
1139
|
+
commentCount: updatedComments.length
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Clear reply state
|
|
1143
|
+
this.replyingTo.set(null);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Clear the input
|
|
1147
|
+
this.commentText.set('');
|
|
1148
|
+
this.showMentionMenu.set(false);
|
|
1149
|
+
|
|
1150
|
+
// Reset textarea height to initial state
|
|
1151
|
+
if (this.commentInput?.nativeElement) {
|
|
1152
|
+
this.commentInput.nativeElement.style.height = 'auto';
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Blur the input to hide the keyboard
|
|
1156
|
+
this.commentInput?.nativeElement.blur();
|
|
1157
|
+
|
|
1158
|
+
// Hide keyboard explicitly
|
|
1159
|
+
Keyboard.hide().catch(e => console.log('Keyboard.hide() not available'));
|
|
1160
|
+
|
|
1161
|
+
// In a real app, you would also send this to your backend
|
|
1162
|
+
// this.commentService.addComment(currentPost.postId, text);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Open image in lightbox
|
|
1167
|
+
*/
|
|
1168
|
+
openImageLightbox(): void {
|
|
1169
|
+
const postData = this.post();
|
|
1170
|
+
|
|
1171
|
+
if (!postData.imageSrc) return;
|
|
1172
|
+
|
|
1173
|
+
const authorMeta: LightboxAuthor = {
|
|
1174
|
+
name: postData.authorName,
|
|
1175
|
+
role: postData.authorRole,
|
|
1176
|
+
avatarInitials: postData.avatarInitials || '',
|
|
1177
|
+
avatarType: postData.avatarType || 'initials',
|
|
1178
|
+
avatarSrc: postData.avatarSrc || '',
|
|
1179
|
+
timestamp: postData.timestamp
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
this.lightbox.open({
|
|
1183
|
+
images: [
|
|
1184
|
+
{
|
|
1185
|
+
type: 'image',
|
|
1186
|
+
src: postData.imageSrc,
|
|
1187
|
+
alt: postData.imageAlt || 'Post image',
|
|
1188
|
+
title: postData.imageAlt || '',
|
|
1189
|
+
description: postData.content,
|
|
1190
|
+
isLiked: postData.isLiked || false,
|
|
1191
|
+
likeCount: postData.likeCount || 0,
|
|
1192
|
+
commentCount: postData.commentCount || 0
|
|
1193
|
+
}
|
|
1194
|
+
],
|
|
1195
|
+
author: authorMeta,
|
|
1196
|
+
enableZoom: true,
|
|
1197
|
+
showControls: false,
|
|
1198
|
+
showInfo: true
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Handle long press on a comment to show action sheet
|
|
1204
|
+
*/
|
|
1205
|
+
async handleCommentLongPress(authorName: string, content: string, isOwnComment: boolean): Promise<void> {
|
|
1206
|
+
const sheet = await this.bottomSheet.create({
|
|
1207
|
+
component: DsMobileCommentActionsBottomSheetComponent,
|
|
1208
|
+
componentProps: {
|
|
1209
|
+
isOwnContent: isOwnComment
|
|
1210
|
+
},
|
|
1211
|
+
breakpoints: [0, 1],
|
|
1212
|
+
initialBreakpoint: 1,
|
|
1213
|
+
handle: true,
|
|
1214
|
+
backdropDismiss: true,
|
|
1215
|
+
cssClass: 'auto-height'
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
const result = await sheet.onWillDismiss();
|
|
1219
|
+
|
|
1220
|
+
if (result.role === 'select' && result.data) {
|
|
1221
|
+
const action = (result.data as CommentActionResult).action;
|
|
1222
|
+
const currentPost = this.post();
|
|
1223
|
+
|
|
1224
|
+
switch (action) {
|
|
1225
|
+
case 'like':
|
|
1226
|
+
console.log('Like comment by', authorName);
|
|
1227
|
+
// Find and toggle like on the comment
|
|
1228
|
+
const updatedComments = currentPost.comments?.map(comment => {
|
|
1229
|
+
if (comment.authorName === authorName && comment.content === content) {
|
|
1230
|
+
const isLiked = !comment.isLiked;
|
|
1231
|
+
return {
|
|
1232
|
+
...comment,
|
|
1233
|
+
isLiked,
|
|
1234
|
+
likeCount: isLiked ? (comment.likeCount || 0) + 1 : Math.max(0, (comment.likeCount || 0) - 1)
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
return comment;
|
|
1238
|
+
});
|
|
1239
|
+
this.post.set({ ...currentPost, comments: updatedComments });
|
|
1240
|
+
break;
|
|
1241
|
+
case 'reply':
|
|
1242
|
+
console.log('Reply to comment by', authorName);
|
|
1243
|
+
this.handleReply(authorName, content);
|
|
1244
|
+
break;
|
|
1245
|
+
case 'edit':
|
|
1246
|
+
console.log('Edit comment by', authorName);
|
|
1247
|
+
// Find the full comment data to get timestamp
|
|
1248
|
+
const commentToEdit = currentPost.comments?.find(
|
|
1249
|
+
comment => comment.authorName === authorName && comment.content === content
|
|
1250
|
+
);
|
|
1251
|
+
if (commentToEdit) {
|
|
1252
|
+
this.handleEditComment(authorName, content, commentToEdit.timestamp);
|
|
1253
|
+
}
|
|
1254
|
+
break;
|
|
1255
|
+
case 'delete':
|
|
1256
|
+
console.log('Delete comment by', authorName);
|
|
1257
|
+
// Show confirmation before deleting
|
|
1258
|
+
if (confirm('Are you sure you want to delete this comment?')) {
|
|
1259
|
+
const updatedCommentsAfterDelete = currentPost.comments?.filter(
|
|
1260
|
+
comment => !(comment.authorName === authorName && comment.content === content)
|
|
1261
|
+
);
|
|
1262
|
+
this.post.set({
|
|
1263
|
+
...currentPost,
|
|
1264
|
+
comments: updatedCommentsAfterDelete,
|
|
1265
|
+
commentCount: updatedCommentsAfterDelete?.length || 0
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|