@trixwell/ngx-parl 0.0.0-watch

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.
@@ -0,0 +1,894 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, model, computed, Component, Injectable, Pipe, effect, ViewChild, signal, isDevMode } from '@angular/core';
3
+ import { NgClass, NgOptimizedImage, DatePipe } from '@angular/common';
4
+ import { FormsModule } from '@angular/forms';
5
+ import * as i1 from '@angular/material/icon';
6
+ import { MatIcon } from '@angular/material/icon';
7
+ import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
8
+ import * as i2$1 from '@ngneat/transloco';
9
+ import { TranslocoPipe, TranslocoModule, provideTransloco } from '@ngneat/transloco';
10
+ import * as i2 from '@angular/platform-browser';
11
+ import * as i1$1 from '@angular/common/http';
12
+ import { MatDialogContent, MatDialogTitle } from '@angular/material/dialog';
13
+ import { MatProgressSpinner } from '@angular/material/progress-spinner';
14
+ import { of } from 'rxjs';
15
+
16
+ class ChatMessage {
17
+ id;
18
+ chat_id;
19
+ cr_time;
20
+ type;
21
+ user;
22
+ content;
23
+ avatar;
24
+ file_path;
25
+ checked;
26
+ edit = false;
27
+ constructor(data) {
28
+ this.id = data.id;
29
+ this.chat_id = data.chat_id;
30
+ this.cr_time = data.cr_time;
31
+ this.type = data.type;
32
+ this.user = data.user;
33
+ this.content = data.content;
34
+ this.avatar = data.avatar ?? null;
35
+ this.file_path = data.file_path ?? null;
36
+ this.checked = data.checked ?? null;
37
+ }
38
+ get dateSimple() {
39
+ const d = new Date(this.cr_time.replace(' ', 'T'));
40
+ const dd = String(d.getDate()).padStart(2, '0');
41
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
42
+ const yyyy = d.getFullYear();
43
+ return `${dd}.${mm}.${yyyy}`;
44
+ }
45
+ get timeHHmm() {
46
+ const d = new Date(this.cr_time.replace(' ', 'T'));
47
+ const hh = String(d.getHours()).padStart(2, '0');
48
+ const mm = String(d.getMinutes()).padStart(2, '0');
49
+ return `${hh}:${mm}`;
50
+ }
51
+ }
52
+ var MessageType;
53
+ (function (MessageType) {
54
+ MessageType["Incoming"] = "incoming";
55
+ MessageType["Outgoing"] = "outgoing";
56
+ })(MessageType || (MessageType = {}));
57
+
58
+ class ChatMessageComponent {
59
+ iconRegistry;
60
+ sanitizer;
61
+ currentMessage = input.required(...(ngDevMode ? [{ debugName: "currentMessage" }] : []));
62
+ edit = model(false, ...(ngDevMode ? [{ debugName: "edit" }] : []));
63
+ requestEdit = model(null, ...(ngDevMode ? [{ debugName: "requestEdit" }] : []));
64
+ requestDelete = model(null, ...(ngDevMode ? [{ debugName: "requestDelete" }] : []));
65
+ constructor(iconRegistry, sanitizer) {
66
+ this.iconRegistry = iconRegistry;
67
+ this.sanitizer = sanitizer;
68
+ this.iconRegistry.addSvgIcon('checked-message', this.sanitizer.bypassSecurityTrustResourceUrl('assets/ngx-parl/icons/checked-message.svg'));
69
+ this.iconRegistry.addSvgIcon('no-check', this.sanitizer.bypassSecurityTrustResourceUrl('assets/ngx-parl/icons/no-check.svg'));
70
+ this.iconRegistry.addSvgIcon('trash', this.sanitizer.bypassSecurityTrustResourceUrl('assets/ngx-parl/icons/trash.svg'));
71
+ this.iconRegistry.addSvgIcon('icon-edit', this.sanitizer.bypassSecurityTrustResourceUrl('assets/ngx-parl/icons/icon-edit.svg'));
72
+ setTimeout(() => {
73
+ this.currentMessage().checked = true;
74
+ }, 600);
75
+ }
76
+ normalizeSourcePath(sourcePath) {
77
+ const cleanedPath = (sourcePath ?? '').trim();
78
+ if (!cleanedPath) {
79
+ return '';
80
+ }
81
+ if (cleanedPath.startsWith('data:') || cleanedPath.startsWith('blob:') || /^https?:\/\//i.test(cleanedPath)) {
82
+ return cleanedPath;
83
+ }
84
+ const assetsIndex = cleanedPath.indexOf('assets/');
85
+ if (assetsIndex >= 0) {
86
+ return '/' + cleanedPath.slice(assetsIndex);
87
+ }
88
+ return cleanedPath.replace(/^\.{1,2}\//, '/');
89
+ }
90
+ attachments = computed(() => {
91
+ const message = this.currentMessage();
92
+ const filePath = message.file_path;
93
+ if (Array.isArray(filePath)) {
94
+ return filePath.map(p => this.normalizeSourcePath(p)).filter(Boolean);
95
+ }
96
+ const rawFilePath = filePath ?? '';
97
+ if (typeof rawFilePath !== 'string' || !rawFilePath.trim) {
98
+ return [];
99
+ }
100
+ if (rawFilePath.trim().startsWith('[')) {
101
+ try {
102
+ const parsed = JSON.parse(rawFilePath);
103
+ if (Array.isArray(parsed)) {
104
+ return parsed
105
+ .map(item => (typeof item === 'string' ? this.normalizeSourcePath(item) : ''))
106
+ .filter(Boolean);
107
+ }
108
+ }
109
+ catch { }
110
+ }
111
+ if (rawFilePath.startsWith('data:')) {
112
+ return [rawFilePath];
113
+ }
114
+ if (rawFilePath.includes('|')) {
115
+ return rawFilePath.split('|').map(p => this.normalizeSourcePath(p)).filter(Boolean);
116
+ }
117
+ if (rawFilePath.includes(',')) {
118
+ return rawFilePath.split(',').map(p => this.normalizeSourcePath(p)).filter(Boolean);
119
+ }
120
+ return [];
121
+ }, ...(ngDevMode ? [{ debugName: "attachments" }] : []));
122
+ avatarSrc = computed(() => {
123
+ const message = this.currentMessage();
124
+ const fallback = message.type === 'incoming'
125
+ ? 'assets/ngx-parl/icons/avatar_anonym.svg'
126
+ : 'assets/ngx-parl/icons/avatar_manager.svg';
127
+ return message.avatar || fallback;
128
+ }, ...(ngDevMode ? [{ debugName: "avatarSrc" }] : []));
129
+ openContextMenu(event, trigger) {
130
+ event.preventDefault();
131
+ event.stopPropagation();
132
+ trigger.openMenu();
133
+ return this;
134
+ }
135
+ editMessage(message) {
136
+ this.edit.set(true);
137
+ this.requestEdit.set(message);
138
+ return this;
139
+ }
140
+ deleteMessage(message) {
141
+ this.requestDelete.set(message.id);
142
+ queueMicrotask(() => this.requestDelete.set(null));
143
+ return this;
144
+ }
145
+ canDelete(message) {
146
+ return message.type === this.messageType.Outgoing;
147
+ }
148
+ messageType = MessageType;
149
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ChatMessageComponent, deps: [{ token: i1.MatIconRegistry }, { token: i2.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
150
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: ChatMessageComponent, isStandalone: true, selector: "lib-chat-message", inputs: { currentMessage: { classPropertyName: "currentMessage", publicName: "currentMessage", isSignal: true, isRequired: true, transformFunction: null }, edit: { classPropertyName: "edit", publicName: "edit", isSignal: true, isRequired: false, transformFunction: null }, requestEdit: { classPropertyName: "requestEdit", publicName: "requestEdit", isSignal: true, isRequired: false, transformFunction: null }, requestDelete: { classPropertyName: "requestDelete", publicName: "requestDelete", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { edit: "editChange", requestEdit: "requestEditChange", requestDelete: "requestDeleteChange" }, ngImport: i0, template: "<div class=\"message\"\n [ngClass]=\"{\n 'message--outgoing': currentMessage().type === messageType.Outgoing,\n 'message--incoming': currentMessage().type === messageType.Incoming\n }\">\n <div class=\"message__avatar\">\n <img [ngSrc]=\"avatarSrc()\" width=\"36\" height=\"36\" alt=\"avatar\"/>\n </div>\n\n <div class=\"message__body\"\n #menuTrigger=\"matMenuTrigger\"\n [matMenuTriggerFor]=\"messageMenu\"\n (contextmenu)=\"openContextMenu($event, menuTrigger)\"\n (keydown.shift.F10)=\"openContextMenu($event, menuTrigger)\">\n\n @if (attachments().length) {\n <div class=\"message__attachments\">\n @for (file of attachments(); track file) {\n <img [src]=\"file\"\n alt=\"attachment\"\n class=\"message__image\"\n loading=\"lazy\"/>\n }\n </div>\n }\n\n <div class=\"message__bubble\" tabindex=\"0\">\n <div class=\"message__text\">{{ currentMessage().content }}</div>\n\n <div class=\"message__meta\">\n <time class=\"message__time\">{{ currentMessage().cr_time | date:'HH:mm' }}</time>\n @if (currentMessage().type === messageType.Outgoing) {\n @if (currentMessage().checked) {\n <img ngSrc=\"assets/ngx-parl/icons/checked-message.svg\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n } @else {\n <img ngSrc=\"assets/ngx-parl/icons/no-check\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n }\n }\n </div>\n </div>\n </div>\n</div>\n\n<mat-menu #messageMenu=\"matMenu\" xPosition=\"before\" yPosition=\"below\" class=\"message__menu\">\n @if (currentMessage().type === messageType.Outgoing) {\n <button mat-menu-item (click)=\"editMessage(currentMessage())\">\n <img ngSrc=\"assets/ngx-parl/icons/icon-edit\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n <span>{{ 'chat.edit' | transloco }}</span>\n </button>\n }\n @if (canDelete(currentMessage())) {\n <button mat-menu-item (click)=\"deleteMessage(currentMessage())\">\n <img ngSrc=\"assets/ngx-parl/icons/trash\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n <span>{{ 'chat.remove' | transloco }}</span>\n </button>\n }\n</mat-menu>\n", styles: [".flow-theme-primary ::ng-deep .modal-chat__header{background-color:#5a72d7;color:#fff}.flow-theme-primary ::ng-deep .message--incoming .message__body{background:#e9ecef;color:#343a40}.flow-theme-primary ::ng-deep .message--outgoing .message__body{background:#4656ca;color:#fff}:root{--shadow-sm: 0px 1px 2px 0px rgba(0, 0, 0, .05);--shadow-md: 0px 4px 12px rgba(0, 0, 0, .08);--shadow-lg: 0px 8px 24px rgba(0, 0, 0, .12);--shadow-soft: 0px 0px 6px 0px rgba(46, 46, 46, .12) }.flow-theme-secondary ::ng-deep .modal-chat__header{background-color:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--incoming .message__body{background:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--outgoing .message__body{background:#efefef;color:#464646}.message{display:flex;gap:8px;align-items:flex-end;max-width:80%;animation:pop .12s ease-out}.message__avatar img{display:block;border-radius:1000px}.message__body{padding:8px 12px;min-width:0;font:400 16px/24px roboto,sans-serif;border-radius:16px}.message__bubble{display:grid;grid-template-columns:1fr auto;column-gap:8px;align-items:end;max-width:100%;word-break:break-word;white-space:pre-wrap}.message__text{min-width:0}.message__meta{display:inline-flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;line-height:1;margin:0}.message__time{font:12px/14px roboto,sans-serif}.message__attachments{display:grid;gap:8px;justify-content:start;width:100%;margin-bottom:8px}.message__attachments:has(:only-child){grid-template-columns:repeat(1,minmax(0,140px))}.message__attachments:has(:nth-child(2):last-child){grid-template-columns:repeat(2,minmax(0,140px))}.message__attachments:has(:nth-child(3)){grid-template-columns:repeat(3,minmax(0,140px))}.message__image{display:block;width:100%;height:140px;object-fit:cover;border-radius:12px;aspect-ratio:1/1}.message__bubble:after{content:\"\";display:block;clear:both}.message__time{font:12px/14px roboto,sans-serif;min-width:30px}.message__icon{width:14px;height:14px;vertical-align:middle}.message--incoming{margin-right:auto}.message--incoming .message__body{border-bottom-right-radius:16px}.message--incoming .message__bubble{border-top-left-radius:4px}.message--incoming .message__time{color:#adb5bd}.message--outgoing{margin-left:auto;flex-direction:row-reverse}.message--outgoing .message__body{border-bottom-left-radius:16px}.message--outgoing .message__bubble{border-top-right-radius:4px}.message--outgoing .message__time{color:#e9ecef}@keyframes pop{0%{transform:translateY(2px);opacity:0}to{transform:translateY(0);opacity:1}}::ng-deep .mat-mdc-menu-content{background:#fff;width:160px;border-radius:8px;gap:4px;border:1px solid #E1E7F8;padding:8px 4px;box-shadow:0 2px 4px #0000001a;overflow:hidden;font:14px/18px roboto,sans-serif}::ng-deep .mat-mdc-menu-item-text{font:14px/18px roboto,sans-serif!important;font-weight:500!important;color:#343a40!important}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }, { kind: "component", type: MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
151
+ }
152
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ChatMessageComponent, decorators: [{
153
+ type: Component,
154
+ args: [{ selector: 'lib-chat-message', imports: [
155
+ NgClass,
156
+ NgOptimizedImage,
157
+ MatIcon,
158
+ DatePipe,
159
+ MatMenu,
160
+ MatMenuItem,
161
+ MatMenuTrigger,
162
+ TranslocoPipe,
163
+ ], standalone: true, template: "<div class=\"message\"\n [ngClass]=\"{\n 'message--outgoing': currentMessage().type === messageType.Outgoing,\n 'message--incoming': currentMessage().type === messageType.Incoming\n }\">\n <div class=\"message__avatar\">\n <img [ngSrc]=\"avatarSrc()\" width=\"36\" height=\"36\" alt=\"avatar\"/>\n </div>\n\n <div class=\"message__body\"\n #menuTrigger=\"matMenuTrigger\"\n [matMenuTriggerFor]=\"messageMenu\"\n (contextmenu)=\"openContextMenu($event, menuTrigger)\"\n (keydown.shift.F10)=\"openContextMenu($event, menuTrigger)\">\n\n @if (attachments().length) {\n <div class=\"message__attachments\">\n @for (file of attachments(); track file) {\n <img [src]=\"file\"\n alt=\"attachment\"\n class=\"message__image\"\n loading=\"lazy\"/>\n }\n </div>\n }\n\n <div class=\"message__bubble\" tabindex=\"0\">\n <div class=\"message__text\">{{ currentMessage().content }}</div>\n\n <div class=\"message__meta\">\n <time class=\"message__time\">{{ currentMessage().cr_time | date:'HH:mm' }}</time>\n @if (currentMessage().type === messageType.Outgoing) {\n @if (currentMessage().checked) {\n <img ngSrc=\"assets/ngx-parl/icons/checked-message.svg\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n } @else {\n <img ngSrc=\"assets/ngx-parl/icons/no-check\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n }\n }\n </div>\n </div>\n </div>\n</div>\n\n<mat-menu #messageMenu=\"matMenu\" xPosition=\"before\" yPosition=\"below\" class=\"message__menu\">\n @if (currentMessage().type === messageType.Outgoing) {\n <button mat-menu-item (click)=\"editMessage(currentMessage())\">\n <img ngSrc=\"assets/ngx-parl/icons/icon-edit\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n <span>{{ 'chat.edit' | transloco }}</span>\n </button>\n }\n @if (canDelete(currentMessage())) {\n <button mat-menu-item (click)=\"deleteMessage(currentMessage())\">\n <img ngSrc=\"assets/ngx-parl/icons/trash\"\n class=\"message__icon\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n <span>{{ 'chat.remove' | transloco }}</span>\n </button>\n }\n</mat-menu>\n", styles: [".flow-theme-primary ::ng-deep .modal-chat__header{background-color:#5a72d7;color:#fff}.flow-theme-primary ::ng-deep .message--incoming .message__body{background:#e9ecef;color:#343a40}.flow-theme-primary ::ng-deep .message--outgoing .message__body{background:#4656ca;color:#fff}:root{--shadow-sm: 0px 1px 2px 0px rgba(0, 0, 0, .05);--shadow-md: 0px 4px 12px rgba(0, 0, 0, .08);--shadow-lg: 0px 8px 24px rgba(0, 0, 0, .12);--shadow-soft: 0px 0px 6px 0px rgba(46, 46, 46, .12) }.flow-theme-secondary ::ng-deep .modal-chat__header{background-color:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--incoming .message__body{background:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--outgoing .message__body{background:#efefef;color:#464646}.message{display:flex;gap:8px;align-items:flex-end;max-width:80%;animation:pop .12s ease-out}.message__avatar img{display:block;border-radius:1000px}.message__body{padding:8px 12px;min-width:0;font:400 16px/24px roboto,sans-serif;border-radius:16px}.message__bubble{display:grid;grid-template-columns:1fr auto;column-gap:8px;align-items:end;max-width:100%;word-break:break-word;white-space:pre-wrap}.message__text{min-width:0}.message__meta{display:inline-flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;line-height:1;margin:0}.message__time{font:12px/14px roboto,sans-serif}.message__attachments{display:grid;gap:8px;justify-content:start;width:100%;margin-bottom:8px}.message__attachments:has(:only-child){grid-template-columns:repeat(1,minmax(0,140px))}.message__attachments:has(:nth-child(2):last-child){grid-template-columns:repeat(2,minmax(0,140px))}.message__attachments:has(:nth-child(3)){grid-template-columns:repeat(3,minmax(0,140px))}.message__image{display:block;width:100%;height:140px;object-fit:cover;border-radius:12px;aspect-ratio:1/1}.message__bubble:after{content:\"\";display:block;clear:both}.message__time{font:12px/14px roboto,sans-serif;min-width:30px}.message__icon{width:14px;height:14px;vertical-align:middle}.message--incoming{margin-right:auto}.message--incoming .message__body{border-bottom-right-radius:16px}.message--incoming .message__bubble{border-top-left-radius:4px}.message--incoming .message__time{color:#adb5bd}.message--outgoing{margin-left:auto;flex-direction:row-reverse}.message--outgoing .message__body{border-bottom-left-radius:16px}.message--outgoing .message__bubble{border-top-right-radius:4px}.message--outgoing .message__time{color:#e9ecef}@keyframes pop{0%{transform:translateY(2px);opacity:0}to{transform:translateY(0);opacity:1}}::ng-deep .mat-mdc-menu-content{background:#fff;width:160px;border-radius:8px;gap:4px;border:1px solid #E1E7F8;padding:8px 4px;box-shadow:0 2px 4px #0000001a;overflow:hidden;font:14px/18px roboto,sans-serif}::ng-deep .mat-mdc-menu-item-text{font:14px/18px roboto,sans-serif!important;font-weight:500!important;color:#343a40!important}\n"] }]
164
+ }], ctorParameters: () => [{ type: i1.MatIconRegistry }, { type: i2.DomSanitizer }], propDecorators: { currentMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentMessage", required: true }] }], edit: [{ type: i0.Input, args: [{ isSignal: true, alias: "edit", required: false }] }, { type: i0.Output, args: ["editChange"] }], requestEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestEdit", required: false }] }, { type: i0.Output, args: ["requestEditChange"] }], requestDelete: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestDelete", required: false }] }, { type: i0.Output, args: ["requestDeleteChange"] }] } });
165
+
166
+ class UtilsService {
167
+ http;
168
+ constructor(http) {
169
+ this.http = http;
170
+ }
171
+ langToLocale(lang) {
172
+ switch (lang) {
173
+ case 'uk':
174
+ return 'uk-UA';
175
+ case 'en':
176
+ default:
177
+ return 'en-US';
178
+ }
179
+ }
180
+ getLocalISODate() {
181
+ const d = new Date();
182
+ const year = d.getFullYear();
183
+ const month = String(d.getMonth() + 1).padStart(2, '0');
184
+ const day = String(d.getDate()).padStart(2, '0');
185
+ const hours = String(d.getHours()).padStart(2, '0');
186
+ const minutes = String(d.getMinutes()).padStart(2, '0');
187
+ const seconds = String(d.getSeconds()).padStart(2, '0');
188
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
189
+ }
190
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: UtilsService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
191
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: UtilsService, providedIn: 'root' });
192
+ }
193
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: UtilsService, decorators: [{
194
+ type: Injectable,
195
+ args: [{
196
+ providedIn: 'root'
197
+ }]
198
+ }], ctorParameters: () => [{ type: i1$1.HttpClient }] });
199
+
200
+ class ChatStartDayPipe {
201
+ utils;
202
+ transloco;
203
+ constructor(utils, transloco) {
204
+ this.utils = utils;
205
+ this.transloco = transloco;
206
+ }
207
+ transform(value, format = 'd MMMM') {
208
+ if (!value) {
209
+ return '';
210
+ }
211
+ const locale = this.utils.langToLocale(this.transloco.getActiveLang());
212
+ const datePipe = new DatePipe(locale);
213
+ const valueDate = new Date(value);
214
+ const today = new Date();
215
+ const isToday = datePipe.transform(valueDate, 'shortDate') === datePipe.transform(today, 'shortDate');
216
+ return isToday
217
+ ? (locale.startsWith('uk') ? 'Сьогодні' : 'Today')
218
+ : (datePipe.transform(valueDate, format) ?? '');
219
+ }
220
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ChatStartDayPipe, deps: [{ token: UtilsService }, { token: i2$1.TranslocoService }], target: i0.ɵɵFactoryTarget.Pipe });
221
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: ChatStartDayPipe, isStandalone: true, name: "chatStartDay" });
222
+ }
223
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ChatStartDayPipe, decorators: [{
224
+ type: Pipe,
225
+ args: [{
226
+ name: 'chatStartDay'
227
+ }]
228
+ }], ctorParameters: () => [{ type: UtilsService }, { type: i2$1.TranslocoService }] });
229
+
230
+ class ToggleDisplayChatStartDayPipe {
231
+ utils;
232
+ transloco;
233
+ constructor(utils, transloco) {
234
+ this.utils = utils;
235
+ this.transloco = transloco;
236
+ }
237
+ transform(message, messages, i) {
238
+ const locale = this.utils.langToLocale(this.transloco.getActiveLang());
239
+ const datePipe = new DatePipe(locale);
240
+ const prev = i > 0 ? messages[i - 1] : undefined;
241
+ const currDay = datePipe.transform(new Date(message.cr_time), 'shortDate');
242
+ const prevDay = prev ? datePipe.transform(new Date(prev.cr_time), 'shortDate') : undefined;
243
+ return prev ? currDay !== prevDay : true;
244
+ }
245
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToggleDisplayChatStartDayPipe, deps: [{ token: UtilsService }, { token: i2$1.TranslocoService }], target: i0.ɵɵFactoryTarget.Pipe });
246
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.7", ngImport: i0, type: ToggleDisplayChatStartDayPipe, isStandalone: true, name: "toggleDisplayChatStartDay" });
247
+ }
248
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ToggleDisplayChatStartDayPipe, decorators: [{
249
+ type: Pipe,
250
+ args: [{
251
+ name: 'toggleDisplayChatStartDay'
252
+ }]
253
+ }], ctorParameters: () => [{ type: UtilsService }, { type: i2$1.TranslocoService }] });
254
+
255
+ class ChatFlowComponent {
256
+ iconRegistry;
257
+ sanitizer;
258
+ flowRef;
259
+ messageListInput = model.required(...(ngDevMode ? [{ debugName: "messageListInput" }] : []));
260
+ messageList = computed(() => this.messageListInput(), ...(ngDevMode ? [{ debugName: "messageList" }] : []));
261
+ selectedForEdit = model.required(...(ngDevMode ? [{ debugName: "selectedForEdit" }] : []));
262
+ constructor(iconRegistry, sanitizer) {
263
+ this.iconRegistry = iconRegistry;
264
+ this.sanitizer = sanitizer;
265
+ this.iconRegistry.addSvgIcon('lucide_send', this.sanitizer.bypassSecurityTrustResourceUrl('assets/ngx-parl/icons/lucide_send.svg'));
266
+ effect(() => {
267
+ const length = this.messageList().length;
268
+ if (length > 0) {
269
+ queueMicrotask(() => this.scrollToBottomSmooth());
270
+ }
271
+ });
272
+ }
273
+ scrollToBottomSmooth() {
274
+ const element = this.flowRef?.nativeElement;
275
+ if (!element) {
276
+ return this;
277
+ }
278
+ element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
279
+ return this;
280
+ }
281
+ startEdit(message) {
282
+ this.messageList().forEach(currMessage => {
283
+ if (currMessage.id !== message.id && currMessage.edit) {
284
+ currMessage.edit = false;
285
+ }
286
+ });
287
+ message.edit = true;
288
+ if (this.selectedForEdit()?.id === message.id) {
289
+ this.selectedForEdit.set(null);
290
+ queueMicrotask(() => this.selectedForEdit.set(message));
291
+ }
292
+ else {
293
+ this.selectedForEdit.set(message);
294
+ }
295
+ return this;
296
+ }
297
+ onEditChange(id, isEdit) {
298
+ const messageList = this.messageList().find(message => message.id === id);
299
+ if (!messageList) {
300
+ return this;
301
+ }
302
+ if (isEdit) {
303
+ return this.startEdit(messageList);
304
+ }
305
+ else {
306
+ messageList.edit = false;
307
+ if (this.selectedForEdit()?.id === id) {
308
+ this.selectedForEdit.set(null);
309
+ }
310
+ }
311
+ return this;
312
+ }
313
+ onRequestEdit(message) {
314
+ if (message) {
315
+ return this.startEdit(message);
316
+ }
317
+ this.selectedForEdit.set(null);
318
+ return this;
319
+ }
320
+ onRequestDelete(messageId) {
321
+ if (!messageId) {
322
+ return this;
323
+ }
324
+ const updatedList = this.messageList().filter(m => m.id !== messageId);
325
+ this.selectedForEdit.set(null);
326
+ queueMicrotask(() => this.messageListInput.set(updatedList));
327
+ return this;
328
+ }
329
+ trackByMessageId(_index, message) {
330
+ // return message.id;
331
+ return `${message.chat_id}-${message.type}-${message.id}`;
332
+ }
333
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ChatFlowComponent, deps: [{ token: i1.MatIconRegistry }, { token: i2.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
334
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: ChatFlowComponent, isStandalone: true, selector: "app-chat-flow", inputs: { messageListInput: { classPropertyName: "messageListInput", publicName: "messageListInput", isSignal: true, isRequired: true, transformFunction: null }, selectedForEdit: { classPropertyName: "selectedForEdit", publicName: "selectedForEdit", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { messageListInput: "messageListInputChange", selectedForEdit: "selectedForEditChange" }, viewQueries: [{ propertyName: "flowRef", first: true, predicate: ["chatFlowRef"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"chat\">\n @if (messageList()) {\n <div class=\"chat__flow\" #chatFlowRef>\n @for (message of messageList(); track trackByMessageId(i, message); let i = $index) {\n @if (message | toggleDisplayChatStartDay: messageList() : i) {\n <span class=\"chat__start-day\">\n {{ message.cr_time | chatStartDay:'d MMMM' }}\n </span>\n }\n\n <lib-chat-message [currentMessage]=\"message\"\n [edit]=\"message.edit ?? false\"\n (editChange)=\"onEditChange(message.id, $event)\"\n (requestEditChange)=\"onRequestEdit($event)\"\n (requestDeleteChange)=\"onRequestDelete($event)\">\n </lib-chat-message>\n }\n </div>\n } @else {\n <div class=\"chat__flow__empty\">\n <img ngSrc=\"assets/ngx-parl/icons/lucide_send.svg\"\n class=\"message__icon\"\n width=\"18\" height=\"24\" alt=\"hide\" priority/>\n\n\n <div class=\"chat__flow__empty--header\">\n <h3 class=\"chat__flow__empty--title\">{{ 'chat.chat_empty_title' | transloco }}</h3>\n <p class=\"chat__flow__empty--subscription\">{{ 'chat.chat_empty_subscription' | transloco }}</p>\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-height:0}.chat{background:#fff;height:100%;display:flex;flex-direction:column;min-height:0;width:100%}.chat__flow{flex:1 1 auto;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:12px;padding:12px 16px;scroll-behavior:smooth;overscroll-behavior:contain}.chat__flow__empty{display:flex;gap:16px;flex-direction:column;align-items:center;justify-content:center;padding:72px 16px;height:100%}.chat__flow__empty .message__icon{width:56px;height:56px}.chat__flow__empty--header{width:285px;display:flex;gap:12px;flex-direction:column;align-items:center}.chat__flow__empty--title{margin:0;font:500 16px/24px roboto,sans-serif;color:#212529}.chat__flow__empty--subscription{margin:0;font:400 16px/24px roboto,sans-serif;color:#495057}.chat__start-day{display:flex;justify-content:center;background:#f8f9fa;color:#6c757d;font:12px/14px roboto,sans-serif;width:78px;height:22px;margin:0 auto;border-radius:1000px;gap:8px;padding:4px 12px}.chat ::-webkit-scrollbar{width:6px}.chat ::-webkit-scrollbar-track{background:transparent}.chat ::-webkit-scrollbar-thumb{background-color:#e9ecef;border-radius:4px;border:1px solid #E9ECEF}.chat ::-webkit-scrollbar-button{display:none}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "component", type: ChatMessageComponent, selector: "lib-chat-message", inputs: ["currentMessage", "edit", "requestEdit", "requestDelete"], outputs: ["editChange", "requestEditChange", "requestDeleteChange"] }, { kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }, { kind: "pipe", type: ChatStartDayPipe, name: "chatStartDay" }, { kind: "pipe", type: ToggleDisplayChatStartDayPipe, name: "toggleDisplayChatStartDay" }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
335
+ }
336
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ChatFlowComponent, decorators: [{
337
+ type: Component,
338
+ args: [{ selector: 'app-chat-flow', imports: [
339
+ FormsModule,
340
+ ChatMessageComponent,
341
+ ChatStartDayPipe,
342
+ ToggleDisplayChatStartDayPipe,
343
+ ChatMessageComponent,
344
+ TranslocoPipe,
345
+ MatIcon,
346
+ NgOptimizedImage
347
+ ], standalone: true, template: "<div class=\"chat\">\n @if (messageList()) {\n <div class=\"chat__flow\" #chatFlowRef>\n @for (message of messageList(); track trackByMessageId(i, message); let i = $index) {\n @if (message | toggleDisplayChatStartDay: messageList() : i) {\n <span class=\"chat__start-day\">\n {{ message.cr_time | chatStartDay:'d MMMM' }}\n </span>\n }\n\n <lib-chat-message [currentMessage]=\"message\"\n [edit]=\"message.edit ?? false\"\n (editChange)=\"onEditChange(message.id, $event)\"\n (requestEditChange)=\"onRequestEdit($event)\"\n (requestDeleteChange)=\"onRequestDelete($event)\">\n </lib-chat-message>\n }\n </div>\n } @else {\n <div class=\"chat__flow__empty\">\n <img ngSrc=\"assets/ngx-parl/icons/lucide_send.svg\"\n class=\"message__icon\"\n width=\"18\" height=\"24\" alt=\"hide\" priority/>\n\n\n <div class=\"chat__flow__empty--header\">\n <h3 class=\"chat__flow__empty--title\">{{ 'chat.chat_empty_title' | transloco }}</h3>\n <p class=\"chat__flow__empty--subscription\">{{ 'chat.chat_empty_subscription' | transloco }}</p>\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-height:0}.chat{background:#fff;height:100%;display:flex;flex-direction:column;min-height:0;width:100%}.chat__flow{flex:1 1 auto;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:12px;padding:12px 16px;scroll-behavior:smooth;overscroll-behavior:contain}.chat__flow__empty{display:flex;gap:16px;flex-direction:column;align-items:center;justify-content:center;padding:72px 16px;height:100%}.chat__flow__empty .message__icon{width:56px;height:56px}.chat__flow__empty--header{width:285px;display:flex;gap:12px;flex-direction:column;align-items:center}.chat__flow__empty--title{margin:0;font:500 16px/24px roboto,sans-serif;color:#212529}.chat__flow__empty--subscription{margin:0;font:400 16px/24px roboto,sans-serif;color:#495057}.chat__start-day{display:flex;justify-content:center;background:#f8f9fa;color:#6c757d;font:12px/14px roboto,sans-serif;width:78px;height:22px;margin:0 auto;border-radius:1000px;gap:8px;padding:4px 12px}.chat ::-webkit-scrollbar{width:6px}.chat ::-webkit-scrollbar-track{background:transparent}.chat ::-webkit-scrollbar-thumb{background-color:#e9ecef;border-radius:4px;border:1px solid #E9ECEF}.chat ::-webkit-scrollbar-button{display:none}\n"] }]
348
+ }], ctorParameters: () => [{ type: i1.MatIconRegistry }, { type: i2.DomSanitizer }], propDecorators: { flowRef: [{
349
+ type: ViewChild,
350
+ args: ['chatFlowRef', { static: true }]
351
+ }], messageListInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageListInput", required: true }] }, { type: i0.Output, args: ["messageListInputChange"] }], selectedForEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedForEdit", required: true }] }, { type: i0.Output, args: ["selectedForEditChange"] }] } });
352
+
353
+ class ImageFile {
354
+ id;
355
+ url;
356
+ cr_time;
357
+ constructor(id, url, cr_time) {
358
+ this.id = id;
359
+ this.url = url;
360
+ this.cr_time = cr_time;
361
+ }
362
+ }
363
+ var FileType;
364
+ (function (FileType) {
365
+ FileType["IMAGE"] = "image";
366
+ FileType["GIF"] = "gif";
367
+ })(FileType || (FileType = {}));
368
+
369
+ class InputMessageComponent {
370
+ inputTextElement;
371
+ mirrorElement;
372
+ editMessage = input(null, ...(ngDevMode ? [{ debugName: "editMessage" }] : []));
373
+ hasOriginalAttachments = computed(() => {
374
+ const filePaths = this.editFilePaths();
375
+ return filePaths.length > 0;
376
+ }, ...(ngDevMode ? [{ debugName: "hasOriginalAttachments" }] : []));
377
+ hasNewAttachments = computed(() => (this.previews()?.length ?? 0) > 0, ...(ngDevMode ? [{ debugName: "hasNewAttachments" }] : []));
378
+ cancelEdit = model(null, ...(ngDevMode ? [{ debugName: "cancelEdit" }] : []));
379
+ input_text = model('', ...(ngDevMode ? [{ debugName: "input_text" }] : []));
380
+ draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : []));
381
+ focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : []));
382
+ sending = signal(false, ...(ngDevMode ? [{ debugName: "sending" }] : []));
383
+ hasText = computed(() => this.draft().trim().length > 0, ...(ngDevMode ? [{ debugName: "hasText" }] : []));
384
+ isEditMode = computed(() => !!this.editMessage(), ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
385
+ canSend = computed(() => this.hasText() ||
386
+ this.hasNewAttachments() ||
387
+ (this.isEditMode() && this.hasOriginalAttachments()), ...(ngDevMode ? [{ debugName: "canSend" }] : []));
388
+ files = model([], ...(ngDevMode ? [{ debugName: "files" }] : []));
389
+ previews = model([], ...(ngDevMode ? [{ debugName: "previews" }] : []));
390
+ lastHeightPx = 0;
391
+ lastRows = 1;
392
+ resizeRaf = null;
393
+ constructor() {
394
+ effect(() => {
395
+ const message = this.editMessage();
396
+ const element = this.inputTextElement?.nativeElement;
397
+ if (!element) {
398
+ return;
399
+ }
400
+ if (message) {
401
+ const content = message.content ?? '';
402
+ this.draft.set(content);
403
+ element.innerText = content;
404
+ queueMicrotask(() => {
405
+ this.autoResizeByRows();
406
+ element.focus();
407
+ this.focused.set(true);
408
+ });
409
+ }
410
+ });
411
+ }
412
+ ngAfterViewInit() {
413
+ const element = this.inputTextElement.nativeElement;
414
+ element.style.transition = 'height 160ms ease';
415
+ this.initMirror();
416
+ const computedStyle = getComputedStyle(element);
417
+ const lineHeight = this.cssNum(computedStyle.lineHeight, 24);
418
+ element.style.height = `${lineHeight}px`;
419
+ this.lastHeightPx = lineHeight;
420
+ this.lastRows = 1;
421
+ this.updateOverflow(1);
422
+ requestAnimationFrame(() => {
423
+ const { rows, nextHeightPx } = this.measureByMirror();
424
+ element.style.height = `${nextHeightPx}px`;
425
+ this.lastRows = rows;
426
+ this.lastHeightPx = nextHeightPx;
427
+ this.updateOverflow(rows);
428
+ });
429
+ }
430
+ ngOnDestroy() {
431
+ if (this.resizeRaf) {
432
+ cancelAnimationFrame(this.resizeRaf);
433
+ this.resizeRaf = null;
434
+ }
435
+ }
436
+ editFilePaths() {
437
+ const message = this.editMessage();
438
+ if (!message) {
439
+ return [];
440
+ }
441
+ const file_path = message.file_path;
442
+ return Array.isArray(file_path) ? file_path : [];
443
+ }
444
+ collectAttachmentSources() {
445
+ return (this.previews() ?? []).map(p => p.src).filter(Boolean);
446
+ }
447
+ cancelEditMessage() {
448
+ const message = this.editMessage();
449
+ this.cancelEdit.set(message?.id ?? null);
450
+ queueMicrotask(() => this.cancelEdit.set(null));
451
+ this.draft.set('');
452
+ const element = this.inputTextElement?.nativeElement;
453
+ if (element) {
454
+ element.innerHTML = '';
455
+ this.autoResizeByRows();
456
+ element.focus();
457
+ }
458
+ return this;
459
+ }
460
+ enterDown() {
461
+ const element = this.inputTextElement.nativeElement;
462
+ const text = this.draft().trim();
463
+ if (!this.canSend())
464
+ return this;
465
+ this.sending.set(true);
466
+ const files = this.collectAttachmentSources();
467
+ const message = this.editMessage();
468
+ if (message) {
469
+ this.input_text.set({ id: message.id, content: text, files: files.length ? files : undefined });
470
+ }
471
+ else {
472
+ this.input_text.set({ content: text, files: files.length ? files : undefined });
473
+ }
474
+ this.draft.set('');
475
+ element.innerHTML = '';
476
+ this.files.set([]);
477
+ this.previews.set([]);
478
+ element.focus();
479
+ this.autoResizeByRows();
480
+ setTimeout(() => this.sending.set(false), 150);
481
+ return this;
482
+ }
483
+ onFocus() {
484
+ if (this.inputTextElement.nativeElement.innerHTML === '<br>') {
485
+ this.inputTextElement.nativeElement.innerHTML = '';
486
+ }
487
+ this.focused.set(true);
488
+ return this;
489
+ }
490
+ onBlur() {
491
+ this.focused.set(false);
492
+ return this;
493
+ }
494
+ onKeyDown(event) {
495
+ if (event.key === 'Enter' && !event.shiftKey) {
496
+ event.preventDefault();
497
+ this.enterDown();
498
+ return this;
499
+ }
500
+ queueMicrotask(() => this.autoResizeByRows());
501
+ return this;
502
+ }
503
+ onInput() {
504
+ this.draft.set(this.inputTextElement.nativeElement.innerText ?? '');
505
+ this.autoResizeByRows();
506
+ return this;
507
+ }
508
+ onPaste() {
509
+ queueMicrotask(() => {
510
+ this.draft.set(this.inputTextElement.nativeElement.innerText ?? '');
511
+ this.autoResizeByRows();
512
+ });
513
+ return this;
514
+ }
515
+ inputFileChange(event) {
516
+ const inputEl = event.target;
517
+ const selected = inputEl.files;
518
+ if (!selected?.length) {
519
+ inputEl.value = '';
520
+ return this;
521
+ }
522
+ const list = Array.from(selected).filter(f => (f.type || '').startsWith('image/'));
523
+ if (!list.length) {
524
+ inputEl.value = '';
525
+ return this;
526
+ }
527
+ this.files.set([...(this.files() ?? []), ...list]);
528
+ Promise.all(list.map(async (f) => {
529
+ const src = await this.readFileAsDataURL(f);
530
+ const originalKind = (f.type || '') === 'image/gif' ? FileType.GIF : FileType.IMAGE;
531
+ return { src, originalKind, name: f.name, type: f.type || '', size: f.size };
532
+ }))
533
+ .then(items => this.previews.set([...(this.previews() ?? []), ...items]))
534
+ .finally(() => (inputEl.value = ''));
535
+ return this;
536
+ }
537
+ removeFile(index, event) {
538
+ event?.stopPropagation();
539
+ event?.preventDefault();
540
+ const previews = [...(this.previews() ?? [])];
541
+ previews.splice(index, 1);
542
+ this.previews.set(previews);
543
+ const filesArr = [...(this.files() ?? [])];
544
+ if (index >= 0 && index < filesArr.length) {
545
+ filesArr.splice(index, 1);
546
+ this.files.set(filesArr);
547
+ }
548
+ return this;
549
+ }
550
+ openPreview(_item, _index) {
551
+ return this;
552
+ }
553
+ autoResizeByRows() {
554
+ const element = this.inputTextElement.nativeElement;
555
+ const { rows, nextHeightPx } = this.measureByMirror();
556
+ if (rows === this.lastRows) {
557
+ this.updateOverflow(rows);
558
+ return this;
559
+ }
560
+ if (this.resizeRaf) {
561
+ cancelAnimationFrame(this.resizeRaf);
562
+ }
563
+ element.style.height = `${this.lastHeightPx}px`;
564
+ this.resizeRaf = requestAnimationFrame(() => {
565
+ element.style.height = `${nextHeightPx}px`;
566
+ this.lastHeightPx = nextHeightPx;
567
+ this.lastRows = rows;
568
+ this.updateOverflow(rows);
569
+ });
570
+ return this;
571
+ }
572
+ measureByMirror() {
573
+ const inputEl = this.inputTextElement.nativeElement;
574
+ const mirrorEl = this.mirrorElement.nativeElement;
575
+ const computedStyle = getComputedStyle(inputEl);
576
+ let text = inputEl.innerText;
577
+ if (!text || text === '\n') {
578
+ text = '\u00A0';
579
+ }
580
+ mirrorEl.style.width = computedStyle.width;
581
+ mirrorEl.textContent = text;
582
+ const lineHeight = this.cssNum(computedStyle.lineHeight, 24);
583
+ const paddingTop = this.cssNum(computedStyle.paddingTop, 0);
584
+ const paddingBottom = this.cssNum(computedStyle.paddingBottom, 0);
585
+ const paddingY = paddingTop + paddingBottom;
586
+ const maxRowsCss = computedStyle.getPropertyValue('--max-rows').trim();
587
+ const maxRows = maxRowsCss ? this.cssNum(maxRowsCss, 8) : 8;
588
+ const contentH = mirrorEl.offsetHeight;
589
+ const rawRows = Math.max(1, Math.ceil(contentH / lineHeight));
590
+ const rows = Math.min(rawRows, maxRows);
591
+ const nextHeightPx = Math.round(rows * lineHeight + paddingY);
592
+ return { rows, nextHeightPx };
593
+ }
594
+ initMirror() {
595
+ const mirror = this.mirrorElement.nativeElement;
596
+ const input = this.inputTextElement.nativeElement;
597
+ const computedStyle = getComputedStyle(input);
598
+ mirror.style.position = 'absolute';
599
+ mirror.style.visibility = 'hidden';
600
+ mirror.style.pointerEvents = 'none';
601
+ mirror.style.zIndex = '-1';
602
+ mirror.style.whiteSpace = 'pre-wrap';
603
+ mirror.style.overflowWrap = 'break-word';
604
+ mirror.style.wordBreak = 'normal';
605
+ const properties = [
606
+ 'font', 'font-size', 'font-family', 'font-weight', 'font-style',
607
+ 'line-height', 'letter-spacing', 'word-spacing',
608
+ 'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
609
+ 'border-top-width', 'border-bottom-width', 'border-left-width', 'border-right-width',
610
+ 'white-space', 'text-transform', 'box-sizing'
611
+ ];
612
+ properties.forEach(property => mirror.style[property] = computedStyle.getPropertyValue(property));
613
+ mirror.style.paddingTop = '0px';
614
+ mirror.style.paddingBottom = '0px';
615
+ }
616
+ updateOverflow(rows) {
617
+ const element = this.inputTextElement.nativeElement;
618
+ const computedStyle = getComputedStyle(element);
619
+ const maxRowsCss = computedStyle.getPropertyValue('--max-rows').trim();
620
+ const maxRows = maxRowsCss ? this.cssNum(maxRowsCss, 8) : 8;
621
+ element.style.overflowY = rows >= maxRows ? 'auto' : 'hidden';
622
+ return this;
623
+ }
624
+ readFileAsDataURL(file) {
625
+ return new Promise((resolve, reject) => {
626
+ const reader = new FileReader();
627
+ reader.onload = e => resolve(e.target?.result || '');
628
+ reader.onerror = reject;
629
+ reader.readAsDataURL(file);
630
+ });
631
+ }
632
+ cssNum(v, fb = 0) {
633
+ const n = parseFloat(v);
634
+ return Number.isFinite(n) ? n : fb;
635
+ }
636
+ FileType = FileType;
637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: InputMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
638
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: InputMessageComponent, isStandalone: true, selector: "app-input-message", inputs: { editMessage: { classPropertyName: "editMessage", publicName: "editMessage", isSignal: true, isRequired: false, transformFunction: null }, cancelEdit: { classPropertyName: "cancelEdit", publicName: "cancelEdit", isSignal: true, isRequired: false, transformFunction: null }, input_text: { classPropertyName: "input_text", publicName: "input_text", isSignal: true, isRequired: false, transformFunction: null }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null }, previews: { classPropertyName: "previews", publicName: "previews", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cancelEdit: "cancelEditChange", input_text: "input_textChange", files: "filesChange", previews: "previewsChange" }, viewQueries: [{ propertyName: "inputTextElement", first: true, predicate: ["inputText"], descendants: true }, { propertyName: "mirrorElement", first: true, predicate: ["mirror"], descendants: true }], ngImport: i0, template: "<section class=\"message\"\n [class.message--focus]=\"focused()\"\n [class.message--filled]=\"hasText()\"\n [class.message--sending]=\"sending()\">\n\n @if (isEditMode()) {\n <div class=\"message__wrap\">\n <div class=\"message__wrap--input\">\n <button class=\"message__button message__button--attach\">\n <img ngSrc=\"assets/ngx-parl/icons/icon-edit.svg\"\n class=\"message__icon message__icon--edit\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n\n <div class=\"message__edit-hint\">\n <p>{{ editMessage()?.content }}</p>\n </div>\n </div>\n\n <button class=\"message__button message__button--close\"\n type=\"button\"\n (click)=\"cancelEditMessage()\">\n <img ngSrc=\"assets/ngx-parl/icons/close.svg\"\n class=\"message__icon message__icon--close\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n </div>\n }\n\n <div class=\"message__wrap\">\n <div class=\"message__wrap--input\">\n <button type=\"button\"\n class=\"message__button message__button--attach\"\n [attr.aria-label]=\"'chat.attach' | transloco\"\n (click)=\"fileDialog.click()\">\n <img ngSrc=\"assets/ngx-parl/icons/attach-filled.svg\"\n class=\"message__icon message__icon--attach\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n\n <input #fileDialog\n type=\"file\"\n class=\"hidden\"\n accept=\"image/*\"\n multiple\n (change)=\"inputFileChange($event)\">\n\n <div #inputText\n role=\"textbox\"\n class=\"message__input\"\n spellcheck=\"true\"\n contenteditable=\"true\"\n aria-multiline=\"true\"\n [attr.data-placeholder]=\"isEditMode()\n ? ('chat.edit_placeholder' | transloco)\n : ('chat.placeholder' | transloco)\"\n (blur)=\"onBlur()\"\n (focus)=\"onFocus()\"\n (input)=\"onInput()\"\n (paste)=\"onPaste()\"\n (keydown)=\"onKeyDown($event)\">\n </div>\n\n <div #mirror class=\"message__input-mirror\" aria-hidden=\"true\"></div>\n\n <ng-content></ng-content>\n </div>\n\n <button class=\"message__button message__button--send\"\n type=\"button\"\n (click)=\"enterDown()\"\n [attr.aria-label]=\"'chat.send' | transloco\"\n [disabled]=\"!canSend()\">\n <img ngSrc=\"assets/ngx-parl/icons/send.svg\"\n class=\"message__icon message__icon--send\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n\n </button>\n </div>\n\n @if (previews().length) {\n <div class=\"message__files\">\n @for (item of previews(); track $index) {\n <div class=\"message__file\" [attr.data-kind]=\"item.originalKind\">\n <button type=\"button\"\n class=\"message__file-remove\"\n [attr.aria-label]=\"'chat.delete_file' | transloco\"\n (click)=\"removeFile($index, $event)\">\n <img ngSrc=\"assets/ngx-parl/icons/remove.svg\"\n class=\"message__icon message__icon--file\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n\n <div class=\"message__file-media\" (click)=\"openPreview(item, $index)\">\n <img [src]=\"item.src\" [alt]=\"item.name\" class=\"message__file-img\"/>\n\n @if (item.originalKind === FileType.GIF) {\n <span class=\"message__file-badge message__file-badge--gif\">\n {{ 'chat.gif' | transloco }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n }\n</section>\n\n\n", styles: [":host{display:block}.message{--lh: 24px;--max-rows: 8;display:flex;flex-direction:column;justify-content:space-between;background:#fff;border-top:1px solid #E1E7F8;padding:4px 8px;margin-top:8px;gap:8px;border-bottom-left-radius:16px;border-bottom-right-radius:16px;transition:box-shadow .18s ease,background-color .18s ease,border-color .18s ease}.message__wrap{display:flex;width:100%;min-width:0}.message__wrap--input{position:relative;display:flex;align-items:center;gap:12px;flex:1;min-width:0;padding:8px 0}.message__wrap--input:focus-within{border-color:#f8f9fa}.message__wrap--input::-webkit-scrollbar{width:4px;height:4px}.message__wrap--input::-webkit-scrollbar-track{background:transparent}.message__wrap--input::-webkit-scrollbar-thumb{background-color:#e9ecef;border-radius:4px;border:1px solid #E9ECEF}.message__wrap--input::-webkit-scrollbar-button{display:none}.message__input{flex:1 1 auto;min-height:var(--lh);line-height:var(--lh);font:400 16px/24px roboto,sans-serif;outline:none;border:none;background:transparent;color:#343a40;box-sizing:content-box;resize:none;white-space:pre-wrap;overflow-wrap:break-word;border-radius:8px;caret-color:#3c46b9;transition:height .16s ease;padding:0 4px 0 0;overflow-y:auto;max-height:calc(var(--lh) * var(--max-rows))}.message__input:empty:before{content:attr(data-placeholder);color:#ced4da;pointer-events:none;display:inline-block;animation:subtle-rise .18s ease both}.message__input-mirror{position:absolute;visibility:hidden;pointer-events:none;z-index:-1;white-space:pre-wrap;overflow-wrap:break-word;word-break:normal}.message__button{cursor:pointer;background:none;border:none;transition:transform .12s ease,opacity .12s ease,background-color .12s ease}.message__button:active{transform:scale(.96)}.message__button:disabled{opacity:.45;pointer-events:none}.message__icon{display:inline-flex;width:22px;height:22px}.message__icon:not(.message__icon--file):hover{filter:invert(33%) sepia(79%) saturate(2933%) hue-rotate(205deg) brightness(93%) contrast(91%)}.message__icon--focus,.message__icon--filled{filter:invert(33%) sepia(79%) saturate(2933%) hue-rotate(205deg) brightness(93%) contrast(91%)}.message__icon--edit{filter:invert(33%) sepia(79%) saturate(2933%) hue-rotate(205deg) brightness(73%) contrast(99%)}.message__icon--close{filter:invert(1%) sepia(1%) saturate(1%) hue-rotate(100deg) brightness(100%) contrast(30%)}.message__files{display:flex;flex-wrap:wrap;gap:8px}.message__file{position:relative;width:40px;height:40px;flex:0 0 auto;overflow:visible}.message__file-media{width:100%;height:100%;border-radius:8px;overflow:hidden;cursor:pointer;position:relative;background:#f8f9fa}.message__file-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}.message__file-badge{position:absolute;left:6px;top:6px;z-index:2;display:inline-flex;align-items:center;justify-content:center;height:18px;padding:0 6px;border-radius:4px;font:400 16px/24px roboto,sans-serif;background:#0009;color:#fff;letter-spacing:.3px;line-height:1}.message__file-badge--video{width:28px;height:28px;padding:0;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:50%;background:#00000073}.message__file-badge--video mat-icon{width:18px;height:18px;filter:invert(100%)}.message__file-badge--gif{font-weight:700}.message__file-duration{position:absolute;right:6px;bottom:6px;background:#0009;color:#fff;border-radius:4px;padding:1px;font:400 16px/24px roboto,sans-serif;z-index:2px;line-height:1.1}.message__file-remove{position:absolute;top:-6px;right:-6px;width:16px;height:16px;padding:0;border:none;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;z-index:3;cursor:pointer;transition:transform .12s ease,box-shadow .12s ease}.message__file-remove:hover{transform:scale(1.05)}.message__file-remove:active{transform:scale(.96)}.message__file-remove .message__icon--file{width:14px;height:14px}.message__edit-hint{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:8px;border-radius:4px;border-left:2px solid #4656CA;padding:4px 8px}.message__edit-hint p{color:#ced4da;font:400 16px/24px roboto,sans-serif;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hidden{position:absolute;opacity:0;width:0;height:0;pointer-events:none}@keyframes subtle-rise{0%{transform:translateY(2px);opacity:.6}to{transform:translateY(0);opacity:1}}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
639
+ }
640
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: InputMessageComponent, decorators: [{
641
+ type: Component,
642
+ args: [{ selector: 'app-input-message', imports: [TranslocoPipe, NgOptimizedImage], standalone: true, template: "<section class=\"message\"\n [class.message--focus]=\"focused()\"\n [class.message--filled]=\"hasText()\"\n [class.message--sending]=\"sending()\">\n\n @if (isEditMode()) {\n <div class=\"message__wrap\">\n <div class=\"message__wrap--input\">\n <button class=\"message__button message__button--attach\">\n <img ngSrc=\"assets/ngx-parl/icons/icon-edit.svg\"\n class=\"message__icon message__icon--edit\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n\n <div class=\"message__edit-hint\">\n <p>{{ editMessage()?.content }}</p>\n </div>\n </div>\n\n <button class=\"message__button message__button--close\"\n type=\"button\"\n (click)=\"cancelEditMessage()\">\n <img ngSrc=\"assets/ngx-parl/icons/close.svg\"\n class=\"message__icon message__icon--close\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n </div>\n }\n\n <div class=\"message__wrap\">\n <div class=\"message__wrap--input\">\n <button type=\"button\"\n class=\"message__button message__button--attach\"\n [attr.aria-label]=\"'chat.attach' | transloco\"\n (click)=\"fileDialog.click()\">\n <img ngSrc=\"assets/ngx-parl/icons/attach-filled.svg\"\n class=\"message__icon message__icon--attach\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n\n <input #fileDialog\n type=\"file\"\n class=\"hidden\"\n accept=\"image/*\"\n multiple\n (change)=\"inputFileChange($event)\">\n\n <div #inputText\n role=\"textbox\"\n class=\"message__input\"\n spellcheck=\"true\"\n contenteditable=\"true\"\n aria-multiline=\"true\"\n [attr.data-placeholder]=\"isEditMode()\n ? ('chat.edit_placeholder' | transloco)\n : ('chat.placeholder' | transloco)\"\n (blur)=\"onBlur()\"\n (focus)=\"onFocus()\"\n (input)=\"onInput()\"\n (paste)=\"onPaste()\"\n (keydown)=\"onKeyDown($event)\">\n </div>\n\n <div #mirror class=\"message__input-mirror\" aria-hidden=\"true\"></div>\n\n <ng-content></ng-content>\n </div>\n\n <button class=\"message__button message__button--send\"\n type=\"button\"\n (click)=\"enterDown()\"\n [attr.aria-label]=\"'chat.send' | transloco\"\n [disabled]=\"!canSend()\">\n <img ngSrc=\"assets/ngx-parl/icons/send.svg\"\n class=\"message__icon message__icon--send\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n\n </button>\n </div>\n\n @if (previews().length) {\n <div class=\"message__files\">\n @for (item of previews(); track $index) {\n <div class=\"message__file\" [attr.data-kind]=\"item.originalKind\">\n <button type=\"button\"\n class=\"message__file-remove\"\n [attr.aria-label]=\"'chat.delete_file' | transloco\"\n (click)=\"removeFile($index, $event)\">\n <img ngSrc=\"assets/ngx-parl/icons/remove.svg\"\n class=\"message__icon message__icon--file\"\n width=\"18\" height=\"18\" alt=\"hide\" priority/>\n </button>\n\n <div class=\"message__file-media\" (click)=\"openPreview(item, $index)\">\n <img [src]=\"item.src\" [alt]=\"item.name\" class=\"message__file-img\"/>\n\n @if (item.originalKind === FileType.GIF) {\n <span class=\"message__file-badge message__file-badge--gif\">\n {{ 'chat.gif' | transloco }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n }\n</section>\n\n\n", styles: [":host{display:block}.message{--lh: 24px;--max-rows: 8;display:flex;flex-direction:column;justify-content:space-between;background:#fff;border-top:1px solid #E1E7F8;padding:4px 8px;margin-top:8px;gap:8px;border-bottom-left-radius:16px;border-bottom-right-radius:16px;transition:box-shadow .18s ease,background-color .18s ease,border-color .18s ease}.message__wrap{display:flex;width:100%;min-width:0}.message__wrap--input{position:relative;display:flex;align-items:center;gap:12px;flex:1;min-width:0;padding:8px 0}.message__wrap--input:focus-within{border-color:#f8f9fa}.message__wrap--input::-webkit-scrollbar{width:4px;height:4px}.message__wrap--input::-webkit-scrollbar-track{background:transparent}.message__wrap--input::-webkit-scrollbar-thumb{background-color:#e9ecef;border-radius:4px;border:1px solid #E9ECEF}.message__wrap--input::-webkit-scrollbar-button{display:none}.message__input{flex:1 1 auto;min-height:var(--lh);line-height:var(--lh);font:400 16px/24px roboto,sans-serif;outline:none;border:none;background:transparent;color:#343a40;box-sizing:content-box;resize:none;white-space:pre-wrap;overflow-wrap:break-word;border-radius:8px;caret-color:#3c46b9;transition:height .16s ease;padding:0 4px 0 0;overflow-y:auto;max-height:calc(var(--lh) * var(--max-rows))}.message__input:empty:before{content:attr(data-placeholder);color:#ced4da;pointer-events:none;display:inline-block;animation:subtle-rise .18s ease both}.message__input-mirror{position:absolute;visibility:hidden;pointer-events:none;z-index:-1;white-space:pre-wrap;overflow-wrap:break-word;word-break:normal}.message__button{cursor:pointer;background:none;border:none;transition:transform .12s ease,opacity .12s ease,background-color .12s ease}.message__button:active{transform:scale(.96)}.message__button:disabled{opacity:.45;pointer-events:none}.message__icon{display:inline-flex;width:22px;height:22px}.message__icon:not(.message__icon--file):hover{filter:invert(33%) sepia(79%) saturate(2933%) hue-rotate(205deg) brightness(93%) contrast(91%)}.message__icon--focus,.message__icon--filled{filter:invert(33%) sepia(79%) saturate(2933%) hue-rotate(205deg) brightness(93%) contrast(91%)}.message__icon--edit{filter:invert(33%) sepia(79%) saturate(2933%) hue-rotate(205deg) brightness(73%) contrast(99%)}.message__icon--close{filter:invert(1%) sepia(1%) saturate(1%) hue-rotate(100deg) brightness(100%) contrast(30%)}.message__files{display:flex;flex-wrap:wrap;gap:8px}.message__file{position:relative;width:40px;height:40px;flex:0 0 auto;overflow:visible}.message__file-media{width:100%;height:100%;border-radius:8px;overflow:hidden;cursor:pointer;position:relative;background:#f8f9fa}.message__file-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}.message__file-badge{position:absolute;left:6px;top:6px;z-index:2;display:inline-flex;align-items:center;justify-content:center;height:18px;padding:0 6px;border-radius:4px;font:400 16px/24px roboto,sans-serif;background:#0009;color:#fff;letter-spacing:.3px;line-height:1}.message__file-badge--video{width:28px;height:28px;padding:0;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:50%;background:#00000073}.message__file-badge--video mat-icon{width:18px;height:18px;filter:invert(100%)}.message__file-badge--gif{font-weight:700}.message__file-duration{position:absolute;right:6px;bottom:6px;background:#0009;color:#fff;border-radius:4px;padding:1px;font:400 16px/24px roboto,sans-serif;z-index:2px;line-height:1.1}.message__file-remove{position:absolute;top:-6px;right:-6px;width:16px;height:16px;padding:0;border:none;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;z-index:3;cursor:pointer;transition:transform .12s ease,box-shadow .12s ease}.message__file-remove:hover{transform:scale(1.05)}.message__file-remove:active{transform:scale(.96)}.message__file-remove .message__icon--file{width:14px;height:14px}.message__edit-hint{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:8px;border-radius:4px;border-left:2px solid #4656CA;padding:4px 8px}.message__edit-hint p{color:#ced4da;font:400 16px/24px roboto,sans-serif;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hidden{position:absolute;opacity:0;width:0;height:0;pointer-events:none}@keyframes subtle-rise{0%{transform:translateY(2px);opacity:.6}to{transform:translateY(0);opacity:1}}\n"] }]
643
+ }], ctorParameters: () => [], propDecorators: { inputTextElement: [{
644
+ type: ViewChild,
645
+ args: ['inputText', { static: false }]
646
+ }], mirrorElement: [{
647
+ type: ViewChild,
648
+ args: ['mirror', { static: false }]
649
+ }], editMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "editMessage", required: false }] }], cancelEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelEdit", required: false }] }, { type: i0.Output, args: ["cancelEditChange"] }], input_text: [{ type: i0.Input, args: [{ isSignal: true, alias: "input_text", required: false }] }, { type: i0.Output, args: ["input_textChange"] }], files: [{ type: i0.Input, args: [{ isSignal: true, alias: "files", required: false }] }, { type: i0.Output, args: ["filesChange"] }], previews: [{ type: i0.Input, args: [{ isSignal: true, alias: "previews", required: false }] }, { type: i0.Output, args: ["previewsChange"] }] } });
650
+
651
+ var FlowTheme;
652
+ (function (FlowTheme) {
653
+ FlowTheme["PRIMARY"] = "primary";
654
+ FlowTheme["SECONDARY"] = "secondary";
655
+ })(FlowTheme || (FlowTheme = {}));
656
+
657
+ class NgxParlComponent {
658
+ utils;
659
+ transloco;
660
+ ai_run_in_progress = false;
661
+ lastUpdateKey = null;
662
+ theme = input(FlowTheme.PRIMARY, ...(ngDevMode ? [{ debugName: "theme" }] : []));
663
+ header = input(true, ...(ngDevMode ? [{ debugName: "header" }] : []));
664
+ language = input('en', ...(ngDevMode ? [{ debugName: "language" }] : []));
665
+ messageList = model([], ...(ngDevMode ? [{ debugName: "messageList" }] : []));
666
+ messageUpdate = model(...(ngDevMode ? [undefined, { debugName: "messageUpdate" }] : []));
667
+ selectedForEdit = model(null, ...(ngDevMode ? [{ debugName: "selectedForEdit" }] : []));
668
+ incomingUser = computed(() => {
669
+ return (this.messageList().find((message) => message.type === MessageType.Incoming)?.user ?? '');
670
+ }, ...(ngDevMode ? [{ debugName: "incomingUser" }] : []));
671
+ hideHandler = input(null, ...(ngDevMode ? [{ debugName: "hideHandler" }] : []));
672
+ closeHandler = input(null, ...(ngDevMode ? [{ debugName: "closeHandler" }] : []));
673
+ constructor(utils, transloco) {
674
+ this.utils = utils;
675
+ this.transloco = transloco;
676
+ effect(() => {
677
+ const lang = this.language();
678
+ this.transloco.setActiveLang(lang);
679
+ });
680
+ effect(() => {
681
+ const updatedMessage = this.messageUpdate();
682
+ if (!updatedMessage) {
683
+ return;
684
+ }
685
+ const key = `${updatedMessage.id}-${updatedMessage.cr_time}-${updatedMessage.type}`;
686
+ if (this.lastUpdateKey === key) {
687
+ return;
688
+ }
689
+ this.lastUpdateKey = key;
690
+ if (updatedMessage.type !== MessageType.Incoming) {
691
+ return;
692
+ }
693
+ this.messageList.update((currentList) => {
694
+ const list = [...currentList];
695
+ const incomingIndex = list.findIndex((message) => message.id === updatedMessage.id &&
696
+ message.type === MessageType.Incoming);
697
+ if (incomingIndex > -1) {
698
+ list[incomingIndex] = updatedMessage;
699
+ return list;
700
+ }
701
+ list.push(updatedMessage);
702
+ return list;
703
+ });
704
+ });
705
+ }
706
+ onCancelEdit(messageId) {
707
+ if (messageId != null) {
708
+ this.messageList.update((currentList) => {
709
+ const updatedList = [...currentList];
710
+ const index = updatedList.findIndex((message) => message.id === messageId);
711
+ if (index > -1) {
712
+ updatedList[index].edit = false;
713
+ }
714
+ return updatedList;
715
+ });
716
+ }
717
+ this.selectedForEdit.set(null);
718
+ return this;
719
+ }
720
+ sendMessage(event) {
721
+ if (!event) {
722
+ return this;
723
+ }
724
+ // edit message
725
+ if (typeof event !== 'string' && 'id' in event) {
726
+ const { id, content, files } = event;
727
+ this.messageList.update((currentList) => {
728
+ const updatedList = [...currentList];
729
+ const index = updatedList.findIndex((message) => message.id === id);
730
+ if (index > -1) {
731
+ updatedList[index].content = (content ?? '').trim();
732
+ if (Array.isArray(files)) {
733
+ updatedList[index].file_path = files.length ? files : null;
734
+ }
735
+ updatedList[index].edit = false;
736
+ }
737
+ return updatedList;
738
+ });
739
+ this.selectedForEdit.set(null);
740
+ return this;
741
+ }
742
+ // new message
743
+ if (typeof event === 'string') {
744
+ const text = event.trim();
745
+ if (!text) {
746
+ return this;
747
+ }
748
+ const messages = this.messageList();
749
+ const lastId = messages.at(-1)?.id ?? 0;
750
+ const lastOutgoing = [...messages]
751
+ .reverse()
752
+ .find((message) => message.type === MessageType.Outgoing);
753
+ const dto = {
754
+ id: lastId + 1,
755
+ chat_id: lastOutgoing?.chat_id ?? 1,
756
+ cr_time: this.utils.getLocalISODate(),
757
+ type: MessageType.Outgoing,
758
+ user: lastOutgoing?.user ?? '',
759
+ content: text,
760
+ avatar: lastOutgoing?.avatar ?? null,
761
+ file_path: null,
762
+ checked: false,
763
+ };
764
+ this.messageList.update((list) => [...list, new ChatMessage(dto)]);
765
+ return this;
766
+ }
767
+ // new message + files
768
+ const { content, files } = event;
769
+ const text = (content ?? '').trim();
770
+ const hasFiles = Array.isArray(files) && files.length > 0;
771
+ if (!text && !hasFiles) {
772
+ return this;
773
+ }
774
+ const messages = this.messageList();
775
+ const lastId = messages.at(-1)?.id ?? 0;
776
+ const lastOutgoing = [...messages]
777
+ .reverse()
778
+ .find((message) => message.type === MessageType.Outgoing);
779
+ const dto = {
780
+ id: lastId + 1,
781
+ chat_id: lastOutgoing?.chat_id ?? 1,
782
+ cr_time: this.utils.getLocalISODate(),
783
+ type: MessageType.Outgoing,
784
+ user: lastOutgoing?.user ?? '',
785
+ content: text,
786
+ avatar: lastOutgoing?.avatar ?? null,
787
+ file_path: hasFiles ? files : null,
788
+ checked: false,
789
+ };
790
+ this.messageList.update((list) => [...list, new ChatMessage(dto)]);
791
+ return this;
792
+ }
793
+ onHideClick() {
794
+ const handler = this.hideHandler();
795
+ if (handler) {
796
+ handler();
797
+ }
798
+ return this;
799
+ }
800
+ onCloseClick() {
801
+ const handler = this.closeHandler();
802
+ if (handler) {
803
+ handler();
804
+ }
805
+ return this;
806
+ }
807
+ FlowTheme = FlowTheme;
808
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: NgxParlComponent, deps: [{ token: UtilsService }, { token: i2$1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component });
809
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: NgxParlComponent, isStandalone: true, selector: "ngx-parl", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, messageList: { classPropertyName: "messageList", publicName: "messageList", isSignal: true, isRequired: false, transformFunction: null }, messageUpdate: { classPropertyName: "messageUpdate", publicName: "messageUpdate", isSignal: true, isRequired: false, transformFunction: null }, selectedForEdit: { classPropertyName: "selectedForEdit", publicName: "selectedForEdit", isSignal: true, isRequired: false, transformFunction: null }, hideHandler: { classPropertyName: "hideHandler", publicName: "hideHandler", isSignal: true, isRequired: false, transformFunction: null }, closeHandler: { classPropertyName: "closeHandler", publicName: "closeHandler", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { messageList: "messageListChange", messageUpdate: "messageUpdateChange", selectedForEdit: "selectedForEditChange" }, providers: [], ngImport: i0, template: "<div class=\"modal-chat\" [ngClass]=\"'flow-theme-' + theme()\">\n <div class=\"modal-chat__body\">\n @if (header()) {\n <header class=\"modal-chat__header\" mat-dialog-title>\n <div class=\"modal-chat__hide-block\"></div>\n\n <div class=\"modal-chat__title\">\n <h1 class=\"modal-chat__heading\">{{ 'chat.title' | transloco }} {{ incomingUser() }}</h1>\n </div>\n\n <div class=\"modal-chat__actions\">\n <img ngSrc=\"assets/ngx-parl/icons/hide.svg\" class=\"modal-chat__icon modal-chat__icon--hide\"\n width=\"24\" height=\"24\" alt=\"hide\" priority\n (click)=\"onHideClick()\"/>\n <img ngSrc=\"assets/ngx-parl/icons/close.svg\" class=\"modal-chat__icon modal-chat__icon--close\"\n width=\"24\" height=\"24\" alt=\"close\" priority\n (click)=\"onCloseClick()\"/>\n </div>\n </header>\n }\n\n <mat-dialog-content class=\"modal-chat__content\">\n <app-chat-flow [messageListInput]=\"messageList()\" [(selectedForEdit)]=\"selectedForEdit\"></app-chat-flow>\n\n <app-input-message [editMessage]=\"selectedForEdit()\"\n (cancelEditChange)=\"onCancelEdit($event)\"\n (input_textChange)=\"sendMessage($event)\">\n @if (ai_run_in_progress) {\n <mat-spinner [diameter]=\"30\"></mat-spinner>\n }\n </app-input-message>\n </mat-dialog-content>\n </div>\n</div>\n", styles: [".flow-theme-primary ::ng-deep .modal-chat__header{background-color:#5a72d7;color:#fff}.flow-theme-primary ::ng-deep .message--incoming .message__body{background:#e9ecef;color:#343a40}.flow-theme-primary ::ng-deep .message--outgoing .message__body{background:#4656ca;color:#fff}:root{--shadow-sm: 0px 1px 2px 0px rgba(0, 0, 0, .05);--shadow-md: 0px 4px 12px rgba(0, 0, 0, .08);--shadow-lg: 0px 8px 24px rgba(0, 0, 0, .12);--shadow-soft: 0px 0px 6px 0px rgba(46, 46, 46, .12) }.flow-theme-secondary ::ng-deep .modal-chat__header{background-color:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--incoming .message__body{background:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--outgoing .message__body{background:#efefef;color:#464646}.modal-chat{width:800px;height:600px;border-radius:16px}.modal-chat__body{display:flex;flex-direction:column;height:100%}.modal-chat__header{height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;gap:12px;border-top-left-radius:16px;border-top-right-radius:16px}.modal-chat__hide-block{width:48px;color:#5a72d7}.modal-chat__title .modal-chat__heading{color:#fff;font:500 16px/24px roboto,sans-serif}.modal-chat__actions{display:flex;gap:8px}.modal-chat__content{flex:1 1 auto;display:flex;flex-direction:column}.modal-chat__content app-chat-flow{flex:1 1 auto;min-height:0px;overflow:auto;display:flex;align-items:flex-end}.modal-chat__input{flex:0px 0px auto;border-bottom-left-radius:16px;border-bottom-right-radius:16px}:host ::ng-deep .mat-mdc-dialog-content.modal-chat__content{flex:1 1 auto;display:flex;flex-direction:column;padding:0;max-height:none;overflow:hidden}img{cursor:pointer}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "component", type: MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: ChatFlowComponent, selector: "app-chat-flow", inputs: ["messageListInput", "selectedForEdit"], outputs: ["messageListInputChange", "selectedForEditChange"] }, { kind: "component", type: InputMessageComponent, selector: "app-input-message", inputs: ["editMessage", "cancelEdit", "input_text", "files", "previews"], outputs: ["cancelEditChange", "input_textChange", "filesChange", "previewsChange"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
810
+ }
811
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: NgxParlComponent, decorators: [{
812
+ type: Component,
813
+ args: [{ selector: 'ngx-parl', standalone: true, imports: [
814
+ NgOptimizedImage, NgClass, MatDialogContent, MatDialogTitle, MatProgressSpinner, ChatFlowComponent, InputMessageComponent,
815
+ TranslocoModule,
816
+ TranslocoPipe
817
+ ], providers: [], template: "<div class=\"modal-chat\" [ngClass]=\"'flow-theme-' + theme()\">\n <div class=\"modal-chat__body\">\n @if (header()) {\n <header class=\"modal-chat__header\" mat-dialog-title>\n <div class=\"modal-chat__hide-block\"></div>\n\n <div class=\"modal-chat__title\">\n <h1 class=\"modal-chat__heading\">{{ 'chat.title' | transloco }} {{ incomingUser() }}</h1>\n </div>\n\n <div class=\"modal-chat__actions\">\n <img ngSrc=\"assets/ngx-parl/icons/hide.svg\" class=\"modal-chat__icon modal-chat__icon--hide\"\n width=\"24\" height=\"24\" alt=\"hide\" priority\n (click)=\"onHideClick()\"/>\n <img ngSrc=\"assets/ngx-parl/icons/close.svg\" class=\"modal-chat__icon modal-chat__icon--close\"\n width=\"24\" height=\"24\" alt=\"close\" priority\n (click)=\"onCloseClick()\"/>\n </div>\n </header>\n }\n\n <mat-dialog-content class=\"modal-chat__content\">\n <app-chat-flow [messageListInput]=\"messageList()\" [(selectedForEdit)]=\"selectedForEdit\"></app-chat-flow>\n\n <app-input-message [editMessage]=\"selectedForEdit()\"\n (cancelEditChange)=\"onCancelEdit($event)\"\n (input_textChange)=\"sendMessage($event)\">\n @if (ai_run_in_progress) {\n <mat-spinner [diameter]=\"30\"></mat-spinner>\n }\n </app-input-message>\n </mat-dialog-content>\n </div>\n</div>\n", styles: [".flow-theme-primary ::ng-deep .modal-chat__header{background-color:#5a72d7;color:#fff}.flow-theme-primary ::ng-deep .message--incoming .message__body{background:#e9ecef;color:#343a40}.flow-theme-primary ::ng-deep .message--outgoing .message__body{background:#4656ca;color:#fff}:root{--shadow-sm: 0px 1px 2px 0px rgba(0, 0, 0, .05);--shadow-md: 0px 4px 12px rgba(0, 0, 0, .08);--shadow-lg: 0px 8px 24px rgba(0, 0, 0, .12);--shadow-soft: 0px 0px 6px 0px rgba(46, 46, 46, .12) }.flow-theme-secondary ::ng-deep .modal-chat__header{background-color:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--incoming .message__body{background:#848af5;color:#fefefe}.flow-theme-secondary ::ng-deep .message--outgoing .message__body{background:#efefef;color:#464646}.modal-chat{width:800px;height:600px;border-radius:16px}.modal-chat__body{display:flex;flex-direction:column;height:100%}.modal-chat__header{height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;gap:12px;border-top-left-radius:16px;border-top-right-radius:16px}.modal-chat__hide-block{width:48px;color:#5a72d7}.modal-chat__title .modal-chat__heading{color:#fff;font:500 16px/24px roboto,sans-serif}.modal-chat__actions{display:flex;gap:8px}.modal-chat__content{flex:1 1 auto;display:flex;flex-direction:column}.modal-chat__content app-chat-flow{flex:1 1 auto;min-height:0px;overflow:auto;display:flex;align-items:flex-end}.modal-chat__input{flex:0px 0px auto;border-bottom-left-radius:16px;border-bottom-right-radius:16px}:host ::ng-deep .mat-mdc-dialog-content.modal-chat__content{flex:1 1 auto;display:flex;flex-direction:column;padding:0;max-height:none;overflow:hidden}img{cursor:pointer}\n"] }]
818
+ }], ctorParameters: () => [{ type: UtilsService }, { type: i2$1.TranslocoService }], propDecorators: { theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], messageList: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageList", required: false }] }, { type: i0.Output, args: ["messageListChange"] }], messageUpdate: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageUpdate", required: false }] }, { type: i0.Output, args: ["messageUpdateChange"] }], selectedForEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedForEdit", required: false }] }, { type: i0.Output, args: ["selectedForEditChange"] }], hideHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideHandler", required: false }] }], closeHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeHandler", required: false }] }] } });
819
+
820
+ var chat$1 = {
821
+ title: "Telegram",
822
+ hide: "Hide",
823
+ close: "Close",
824
+ attach: "Attach",
825
+ send: "Send message",
826
+ placeholder: "Type a message...",
827
+ edit_placeholder: "Update a message...",
828
+ delete_file: "Remove file",
829
+ gif: "GIF",
830
+ video: "Video",
831
+ edit: "Edit",
832
+ remove: "Delete",
833
+ chat_empty_title: "No messages",
834
+ chat_empty_subscription: "Your chat history will be displayed here. Write your first message to get started!"
835
+ };
836
+ var en = {
837
+ chat: chat$1
838
+ };
839
+
840
+ var chat = {
841
+ title: "Telegram",
842
+ hide: "Згорнути",
843
+ close: "Закрити",
844
+ attach: "Прикріпити",
845
+ send: "Відправити повідомлення",
846
+ placeholder: "Напишіть повідомлення...",
847
+ edit_placeholder: "Змініть текст повідомлення…",
848
+ delete_file: "Видалити файл",
849
+ gif: "GIF",
850
+ video: "Відео",
851
+ edit: "Редагувати",
852
+ remove: "Видалити",
853
+ chat_empty_title: "Повідомлень ще немає",
854
+ chat_empty_subscription: "Тут буде відображатись ваша історія спілкування. Напишіть перше повідомлення, щоб почати!"
855
+ };
856
+ var uk = {
857
+ chat: chat
858
+ };
859
+
860
+ class ParlTranslocoLoader {
861
+ getTranslation(lang) {
862
+ switch (lang) {
863
+ case 'uk':
864
+ return of(uk);
865
+ case 'en':
866
+ default:
867
+ return of(en);
868
+ }
869
+ }
870
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ParlTranslocoLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
871
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ParlTranslocoLoader });
872
+ }
873
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: ParlTranslocoLoader, decorators: [{
874
+ type: Injectable
875
+ }] });
876
+
877
+ function provideNgxParl() {
878
+ return provideTransloco({
879
+ config: {
880
+ availableLangs: ['en', 'uk'],
881
+ defaultLang: 'en',
882
+ reRenderOnLangChange: true,
883
+ prodMode: !isDevMode(),
884
+ },
885
+ loader: ParlTranslocoLoader,
886
+ });
887
+ }
888
+
889
+ /**
890
+ * Generated bundle index. Do not edit.
891
+ */
892
+
893
+ export { NgxParlComponent, provideNgxParl };
894
+ //# sourceMappingURL=trixwell-ngx-parl.mjs.map