@acorex/components 21.0.3-next.11 → 21.0.3-next.13

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.
@@ -7,10 +7,10 @@ import { InjectionToken, signal, computed, inject, Injectable, input, output, Ch
7
7
  import { AXDialogService } from '@acorex/components/dialog';
8
8
  import { AXPopupService } from '@acorex/components/popup';
9
9
  import * as i3 from '@acorex/core/translation';
10
- import { AXTranslationService, AXTranslationModule, translateSync } from '@acorex/core/translation';
10
+ import { translateSync, AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
11
11
  import { Subject, BehaviorSubject, Observable, filter, firstValueFrom, takeUntil, catchError, EMPTY } from 'rxjs';
12
12
  import { AXUploaderBrowseDirective, AXUploaderZoneDirective, AXUploaderService } from '@acorex/cdk/uploader';
13
- import { createFileTypeMetadata, AXFileTypeInfoProvider, formatFileSizeBytes, AXFileService, AXFileTypeRegistryService, provideFileValidationRules, provideFileTypeInfoProvider, resolveFileCopyText } from '@acorex/core/file';
13
+ import { createFileTypeMetadata, AXFileTypeInfoProvider, formatFileSizeBytes, AXFileService, AXFileTypeRegistryService, provideFileValidationRules, provideFileTypeInfoProvider, resolveFileCopyText, runFileTypeOpen } from '@acorex/core/file';
14
14
  import * as i1$2 from '@acorex/components/progress-bar';
15
15
  import { AXProgressBarModule } from '@acorex/components/progress-bar';
16
16
  import { AXToastService } from '@acorex/components/toast';
@@ -1039,6 +1039,410 @@ function validateLongitude(longitude) {
1039
1039
  return { valid: true };
1040
1040
  }
1041
1041
 
1042
+ class AXConversationMessageUtilsService {
1043
+ /**
1044
+ * Normalize optional avatar/icon values so empty or whitespace-only strings
1045
+ * are treated as missing data.
1046
+ */
1047
+ static normalizeOptionalMediaValue(value) {
1048
+ if (typeof value !== 'string') {
1049
+ return undefined;
1050
+ }
1051
+ const normalized = value.trim();
1052
+ return normalized.length > 0 ? normalized : undefined;
1053
+ }
1054
+ /**
1055
+ * Get conversation avatar image URL.
1056
+ */
1057
+ static getConversationAvatar(conversation) {
1058
+ return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.avatar);
1059
+ }
1060
+ /**
1061
+ * Font Awesome icon class(es) for a conversation when there is no avatar image.
1062
+ */
1063
+ static getConversationAvatarIcon(conversation) {
1064
+ if (AXConversationMessageUtilsService.getConversationAvatar(conversation)) {
1065
+ return undefined;
1066
+ }
1067
+ return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.icon);
1068
+ }
1069
+ /**
1070
+ * Get sender name from message
1071
+ */
1072
+ static getSenderName(message, conversation) {
1073
+ const participant = conversation.participants.find((p) => p.id === message.senderId);
1074
+ return participant?.name || translateSync('@acorex:chat.fallbacks.unknown-user');
1075
+ }
1076
+ /**
1077
+ * Get sender avatar image URL (takes precedence over {@link getSenderAvatarIcon}).
1078
+ */
1079
+ static getSenderAvatar(message, conversation) {
1080
+ const participant = conversation.participants.find((p) => p.id === message.senderId);
1081
+ return AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar);
1082
+ }
1083
+ /**
1084
+ * Font Awesome icon class(es) for the sender when there is no avatar image:
1085
+ * participant `icon` first, then conversation-level `icon`.
1086
+ */
1087
+ static getSenderAvatarIcon(message, conversation) {
1088
+ const participant = conversation.participants.find((p) => p.id === message.senderId);
1089
+ if (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar)) {
1090
+ return undefined;
1091
+ }
1092
+ return (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.icon) ??
1093
+ AXConversationMessageUtilsService.getConversationAvatarIcon(conversation));
1094
+ }
1095
+ /**
1096
+ * Get initials from name
1097
+ */
1098
+ static getInitials(name) {
1099
+ if (!name)
1100
+ return '?';
1101
+ return name
1102
+ .split(' ')
1103
+ .map((n) => n[0])
1104
+ .join('')
1105
+ .toUpperCase()
1106
+ .substring(0, 2);
1107
+ }
1108
+ /**
1109
+ * Type guard for text payload
1110
+ */
1111
+ static isTextPayload(payload) {
1112
+ return 'text' in payload && typeof payload.text === 'string';
1113
+ }
1114
+ /**
1115
+ * Get message text content
1116
+ */
1117
+ static getMessageText(message) {
1118
+ if (message.type === 'text' && AXConversationMessageUtilsService.isTextPayload(message.payload)) {
1119
+ return message.payload.text;
1120
+ }
1121
+ return `[${message.type}]`;
1122
+ }
1123
+ /**
1124
+ * Format message preview text
1125
+ */
1126
+ static getPreviewText(message, maxLength = 50) {
1127
+ // Handle different message types
1128
+ switch (message.type) {
1129
+ case 'text': {
1130
+ const textPayload = message.payload;
1131
+ const text = textPayload.text || '';
1132
+ const truncatedText = text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
1133
+ return {
1134
+ value: truncatedText,
1135
+ type: 'text',
1136
+ icon: 'fa-light fa-message',
1137
+ };
1138
+ }
1139
+ case 'image': {
1140
+ const imagePayload = normalizeImagePayload(message.payload);
1141
+ const first = imagePayload.images[0];
1142
+ const label = imagePayload.caption?.trim() ||
1143
+ (imagePayload.images && imagePayload.images.length > 1
1144
+ ? `${imagePayload.images.length} images`
1145
+ : '');
1146
+ return {
1147
+ value: label || first?.thumbnailUrl || first?.url || '',
1148
+ type: 'image',
1149
+ icon: 'fa-light fa-image',
1150
+ };
1151
+ }
1152
+ case 'video': {
1153
+ const videoPayload = normalizeVideoPayload(message.payload);
1154
+ const first = videoPayload.videos[0];
1155
+ const label = videoPayload.caption?.trim() ||
1156
+ (videoPayload.videos.length > 1 ? `${videoPayload.videos.length} videos` : '');
1157
+ return {
1158
+ value: label || first?.url || '',
1159
+ type: 'video',
1160
+ icon: 'fa-light fa-video',
1161
+ };
1162
+ }
1163
+ case 'audio': {
1164
+ const audioPayload = normalizeAudioPayload(message.payload);
1165
+ const names = audioPayload.audios.map((a) => a.title).filter(Boolean).join(', ');
1166
+ const first = audioPayload.audios[0];
1167
+ const label = audioPayload.caption?.trim() ||
1168
+ (audioPayload.audios.length > 1 ? `${audioPayload.audios.length} audio files` : names);
1169
+ return {
1170
+ value: label || first?.url || '',
1171
+ type: 'audio',
1172
+ icon: 'fa-light fa-music',
1173
+ };
1174
+ }
1175
+ case 'voice': {
1176
+ const voicePayload = message.payload;
1177
+ return {
1178
+ value: voicePayload.url || '',
1179
+ type: 'voice',
1180
+ icon: 'fa-light fa-microphone',
1181
+ };
1182
+ }
1183
+ case 'file': {
1184
+ const filePayload = normalizeFilePayload(message.payload);
1185
+ const names = filePayload.files.map((f) => f.name).join(', ');
1186
+ const label = filePayload.caption?.trim() ||
1187
+ (filePayload.files.length > 1 ? `${filePayload.files.length} files` : names);
1188
+ return {
1189
+ value: label || names,
1190
+ type: 'file',
1191
+ icon: 'fa-light fa-file',
1192
+ };
1193
+ }
1194
+ case 'location': {
1195
+ const locationPayload = message.payload;
1196
+ return {
1197
+ value: locationPayload.latitude && locationPayload.longitude
1198
+ ? `${locationPayload.latitude},${locationPayload.longitude}`
1199
+ : '',
1200
+ type: 'location',
1201
+ icon: 'fa-light fa-location-dot',
1202
+ };
1203
+ }
1204
+ case 'sticker': {
1205
+ const stickerPayload = message.payload;
1206
+ return {
1207
+ value: stickerPayload.url || '',
1208
+ type: 'sticker',
1209
+ icon: 'fa-light fa-face-smile',
1210
+ };
1211
+ }
1212
+ default:
1213
+ return {
1214
+ value: '',
1215
+ type: message.type,
1216
+ icon: 'fa-light fa-message',
1217
+ };
1218
+ }
1219
+ }
1220
+ /**
1221
+ * Check if message is from current user
1222
+ */
1223
+ static isOwnMessage(message, currentUserId) {
1224
+ return message.senderId === currentUserId;
1225
+ }
1226
+ /**
1227
+ * Get message status icon class (Font Awesome)
1228
+ */
1229
+ static getStatusIcon(message) {
1230
+ switch (message.status) {
1231
+ case 'sending':
1232
+ return 'fa-light fa-clock';
1233
+ case 'sent':
1234
+ return 'fa-light fa-check';
1235
+ case 'delivered':
1236
+ return 'fa-light fa-check-double';
1237
+ case 'read':
1238
+ return 'fa-light fa-check-double';
1239
+ case 'failed':
1240
+ return 'fa-light fa-circle-exclamation';
1241
+ default:
1242
+ return 'fa-light fa-check';
1243
+ }
1244
+ }
1245
+ /**
1246
+ * Check if message should show avatar
1247
+ */
1248
+ static shouldShowAvatar(message, previousMessage, conversation) {
1249
+ // Always show avatar for group conversations
1250
+ if (conversation.type === 'group' || conversation.type === 'channel') {
1251
+ // Don't show if same sender as previous message within 5 minutes
1252
+ if (previousMessage && previousMessage.senderId === message.senderId) {
1253
+ const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
1254
+ return timeDiff > 5 * 60 * 1000; // 5 minutes
1255
+ }
1256
+ return true;
1257
+ }
1258
+ return false;
1259
+ }
1260
+ /**
1261
+ * Group messages by sender for consecutive messages
1262
+ */
1263
+ static shouldGroupWithPrevious(message, previousMessage) {
1264
+ if (!previousMessage)
1265
+ return false;
1266
+ // Same sender
1267
+ if (message.senderId !== previousMessage.senderId)
1268
+ return false;
1269
+ // Within 5 minutes
1270
+ const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
1271
+ return timeDiff < 5 * 60 * 1000;
1272
+ }
1273
+ /**
1274
+ * Get conversation status for avatar (private conversations only)
1275
+ */
1276
+ static getConversationStatus(conversation) {
1277
+ if (conversation.type === 'private') {
1278
+ return conversation.status.presence;
1279
+ }
1280
+ return undefined;
1281
+ }
1282
+ /**
1283
+ * Get typing indicator text for conversation
1284
+ */
1285
+ static getTypingText(conversation) {
1286
+ const typingUsers = conversation.status.typingUsers;
1287
+ if (typingUsers.length === 0)
1288
+ return '';
1289
+ if (conversation.type === 'private') {
1290
+ return translateSync('@acorex:chat.status.typing');
1291
+ }
1292
+ const firstUser = conversation.participants.find((p) => p.id === typingUsers[0]);
1293
+ if (typingUsers.length === 1) {
1294
+ return translateSync('@acorex:chat.status.user-is-typing', {
1295
+ params: { userName: firstUser?.name || translateSync('@acorex:chat.fallbacks.someone') },
1296
+ });
1297
+ }
1298
+ return translateSync('@acorex:chat.status.people-typing', { params: { count: typingUsers.length } });
1299
+ }
1300
+ /**
1301
+ * Format last seen time
1302
+ */
1303
+ static formatLastSeen(date) {
1304
+ const now = new Date();
1305
+ const diff = now.getTime() - date.getTime();
1306
+ const seconds = Math.floor(diff / 1000);
1307
+ if (seconds < 60)
1308
+ return translateSync('@acorex:chat.time.just-now');
1309
+ if (seconds < 3600)
1310
+ return translateSync('@acorex:chat.time.minutes-ago', { params: { count: Math.floor(seconds / 60) } });
1311
+ if (seconds < 86400)
1312
+ return translateSync('@acorex:chat.time.hours-ago', { params: { count: Math.floor(seconds / 3600) } });
1313
+ if (seconds < 604800)
1314
+ return translateSync('@acorex:chat.time.days-ago', { params: { count: Math.floor(seconds / 86400) } });
1315
+ return date.toLocaleDateString();
1316
+ }
1317
+ /**
1318
+ * Get conversation subtitle (status or member count)
1319
+ */
1320
+ static getConversationSubtitle(conversation) {
1321
+ if (conversation.status.isTyping) {
1322
+ return AXConversationMessageUtilsService.getTypingText(conversation);
1323
+ }
1324
+ switch (conversation.type) {
1325
+ case 'private':
1326
+ if (conversation.status.presence === 'online') {
1327
+ return translateSync('@acorex:chat.status.online');
1328
+ }
1329
+ if (conversation.status.lastSeen) {
1330
+ return translateSync('@acorex:chat.status.last-seen', {
1331
+ params: { value: AXConversationMessageUtilsService.formatLastSeen(conversation.status.lastSeen) },
1332
+ });
1333
+ }
1334
+ return translateSync('@acorex:chat.status.offline');
1335
+ case 'group':
1336
+ return translateSync('@acorex:chat.members.count', { params: { count: conversation.participants.length } });
1337
+ case 'channel':
1338
+ return translateSync('@acorex:chat.members.subscribers-count', { params: { count: conversation.participants.length } });
1339
+ case 'bot':
1340
+ return translateSync('@acorex:chat.bot');
1341
+ default:
1342
+ return '';
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ /** Other participant in a private chat (excludes the current user). */
1348
+ function resolvePrivatePeerUserId(conversation, currentUserId) {
1349
+ if (conversation.type !== 'private') {
1350
+ return undefined;
1351
+ }
1352
+ const currentId = currentUserId ?? 'current-user';
1353
+ return conversation.participants.find((participant) => participant.id !== currentId)?.id;
1354
+ }
1355
+ /** Whether `auto` kind should render a user avatar for this conversation. */
1356
+ function shouldUseUserAvatarForConversation(conversation, currentUserId) {
1357
+ return conversation.type === 'private' && !!resolvePrivatePeerUserId(conversation, currentUserId);
1358
+ }
1359
+ function resolveUserAvatarDisplay(userId, conversation, message) {
1360
+ if (message && conversation) {
1361
+ return {
1362
+ name: AXConversationMessageUtilsService.getSenderName(message, conversation),
1363
+ avatar: AXConversationMessageUtilsService.getSenderAvatar(message, conversation),
1364
+ icon: AXConversationMessageUtilsService.getSenderAvatarIcon(message, conversation),
1365
+ };
1366
+ }
1367
+ const participant = conversation?.participants.find((p) => p.id === userId);
1368
+ return {
1369
+ name: participant?.name ?? userId,
1370
+ avatar: participant?.avatar,
1371
+ icon: participant?.icon,
1372
+ };
1373
+ }
1374
+ function resolveConversationAvatarDisplay(conversation, currentUserId) {
1375
+ const title = currentUserId !== undefined
1376
+ ? resolveConversationTitleForViewer(conversation, currentUserId)
1377
+ : conversation.title;
1378
+ return {
1379
+ name: title,
1380
+ avatar: AXConversationMessageUtilsService.getConversationAvatar(conversation),
1381
+ icon: AXConversationMessageUtilsService.getConversationAvatarIcon(conversation),
1382
+ };
1383
+ }
1384
+
1385
+ const GENERIC_PRIVATE_TITLES = new Set(['', 'new chat', 'new conversation']);
1386
+ /** True when the stored title is a placeholder, not a user-defined name. */
1387
+ function isGenericPrivateConversationTitle(title) {
1388
+ return GENERIC_PRIVATE_TITLES.has((title ?? '').trim().toLowerCase());
1389
+ }
1390
+ /** Other participant in a private 1v1 chat (excludes the current viewer). */
1391
+ function resolvePrivatePeerParticipant(conversation, currentUserId) {
1392
+ if (conversation.type !== 'private') {
1393
+ return undefined;
1394
+ }
1395
+ const peerId = resolvePrivatePeerUserId(conversation, currentUserId);
1396
+ if (!peerId) {
1397
+ return undefined;
1398
+ }
1399
+ return conversation.participants.find((participant) => participant.id === peerId);
1400
+ }
1401
+ /**
1402
+ * Resolves the display title for the current viewer.
1403
+ * Private 1v1 chats show the other participant's name when no custom title is set.
1404
+ */
1405
+ function resolveConversationTitleForViewer(conversation, currentUserId) {
1406
+ if (conversation.type !== 'private') {
1407
+ return conversation.title;
1408
+ }
1409
+ const peer = resolvePrivatePeerParticipant(conversation, currentUserId);
1410
+ if (peer?.name) {
1411
+ return peer.name;
1412
+ }
1413
+ if (isGenericPrivateConversationTitle(conversation.title)) {
1414
+ return conversation.title || 'New Chat';
1415
+ }
1416
+ return conversation.title;
1417
+ }
1418
+ /**
1419
+ * Returns a viewer-scoped copy of a conversation with dynamic private title/avatar/icon.
1420
+ * Does not mutate the source object.
1421
+ */
1422
+ function resolveConversationForViewer(conversation, currentUserId) {
1423
+ if (conversation.type !== 'private' || !currentUserId) {
1424
+ return conversation;
1425
+ }
1426
+ const peer = resolvePrivatePeerParticipant(conversation, currentUserId);
1427
+ if (!peer) {
1428
+ return conversation;
1429
+ }
1430
+ const title = resolveConversationTitleForViewer(conversation, currentUserId);
1431
+ const avatar = conversation.avatar ?? peer.avatar;
1432
+ const icon = conversation.icon ?? peer.icon;
1433
+ if (title === conversation.title &&
1434
+ avatar === conversation.avatar &&
1435
+ icon === conversation.icon) {
1436
+ return conversation;
1437
+ }
1438
+ return {
1439
+ ...conversation,
1440
+ title,
1441
+ avatar,
1442
+ icon,
1443
+ };
1444
+ }
1445
+
1042
1446
  /**
1043
1447
  * In-memory conversation and message graph (signal-based).
1044
1448
  * Plain class — not DI-registered; instantiated by `AXConversationService`.
@@ -2552,9 +2956,22 @@ class AXComposerService {
2552
2956
  await this.conversationService.sendTypingIndicator(conversationId);
2553
2957
  }
2554
2958
  async sendMessage(command, options) {
2959
+ this.clearComposerInput(command.conversationId);
2555
2960
  await this.conversationService.sendMessage(command, options);
2961
+ }
2962
+ /**
2963
+ * Resets composer input immediately when a send starts so upload/API work continues in the background.
2964
+ */
2965
+ clearComposerInput(conversationId) {
2966
+ this.draftText.set('');
2967
+ this.attachments.set([]);
2556
2968
  this.hideComponent();
2557
- this.clear();
2969
+ this._replyMessage.set(null);
2970
+ this._editMessage.set(null);
2971
+ if (conversationId) {
2972
+ this.clearDraft(conversationId);
2973
+ }
2974
+ this.focusComposer();
2558
2975
  }
2559
2976
  /**
2560
2977
  * Show a component in the composer (full-width mode)
@@ -3558,17 +3975,12 @@ class AXAudioPickerComponent {
3558
3975
  },
3559
3976
  };
3560
3977
  this.retainUploadedOnDismiss = true;
3561
- try {
3562
- await this.composerService.sendMessage(command);
3563
- const popup = this.__popup__();
3564
- if (popup) {
3565
- this.dismissPicker();
3566
- dismissComposerPickerHost(this.composerService, popup);
3567
- }
3568
- }
3569
- catch {
3978
+ const popup = this.__popup__();
3979
+ this.dismissPicker();
3980
+ dismissComposerPickerHost(this.composerService, popup);
3981
+ void this.composerService.sendMessage(command).catch(() => {
3570
3982
  this.retainUploadedOnDismiss = false;
3571
- }
3983
+ });
3572
3984
  }
3573
3985
  canSend() {
3574
3986
  const list = this.audioFiles();
@@ -3976,18 +4388,13 @@ class AXFilePickerComponent {
3976
4388
  caption: this.caption.trim() || undefined,
3977
4389
  },
3978
4390
  };
3979
- this.retainUploadedOnDismiss = true;
3980
- try {
3981
- await this.composerService.sendMessage(command);
3982
- const popup = this.__popup__();
3983
- if (popup) {
3984
- this.dismissPicker();
3985
- dismissComposerPickerHost(this.composerService, popup);
3986
- }
3987
- }
3988
- catch {
4391
+ this.retainUploadedOnDismiss = true;
4392
+ const popup = this.__popup__();
4393
+ this.dismissPicker();
4394
+ dismissComposerPickerHost(this.composerService, popup);
4395
+ void this.composerService.sendMessage(command).catch(() => {
3989
4396
  this.retainUploadedOnDismiss = false;
3990
- }
4397
+ });
3991
4398
  }
3992
4399
  updateFileProgress(index, progress, uploading, uploaded = false, uploadedUrl, mediaId, error) {
3993
4400
  if (this.uploadsDismissed) {
@@ -4439,17 +4846,12 @@ class AXImagePickerComponent {
4439
4846
  },
4440
4847
  };
4441
4848
  this.retainUploadedOnDismiss = true;
4442
- try {
4443
- await this.composerService.sendMessage(command);
4444
- const popup = this.__popup__();
4445
- if (popup) {
4446
- this.dismissPicker();
4447
- dismissComposerPickerHost(this.composerService, popup);
4448
- }
4449
- }
4450
- catch {
4849
+ const popup = this.__popup__();
4850
+ this.dismissPicker();
4851
+ dismissComposerPickerHost(this.composerService, popup);
4852
+ void this.composerService.sendMessage(command).catch(() => {
4451
4853
  this.retainUploadedOnDismiss = false;
4452
- }
4854
+ });
4453
4855
  }
4454
4856
  canSend() {
4455
4857
  const list = this.images();
@@ -4705,8 +5107,10 @@ class AXLocationPickerComponent {
4705
5107
  type: 'location',
4706
5108
  payload: location,
4707
5109
  };
4708
- await this.composerService.sendMessage(command);
4709
5110
  this.cancel();
5111
+ void this.composerService.sendMessage(command).catch((error) => {
5112
+ console.error('Failed to send location:', error);
5113
+ });
4710
5114
  }
4711
5115
  cancel() {
4712
5116
  // Reset to default location
@@ -4955,17 +5359,12 @@ class AXVideoPickerComponent {
4955
5359
  },
4956
5360
  };
4957
5361
  this.retainUploadedOnDismiss = true;
4958
- try {
4959
- await this.composerService.sendMessage(command);
4960
- const popup = this.__popup__();
4961
- if (popup) {
4962
- this.dismissPicker();
4963
- dismissComposerPickerHost(this.composerService, popup);
4964
- }
4965
- }
4966
- catch {
5362
+ const popup = this.__popup__();
5363
+ this.dismissPicker();
5364
+ dismissComposerPickerHost(this.composerService, popup);
5365
+ void this.composerService.sendMessage(command).catch(() => {
4967
5366
  this.retainUploadedOnDismiss = false;
4968
- }
5367
+ });
4969
5368
  }
4970
5369
  canSend() {
4971
5370
  const list = this.videos();
@@ -5372,44 +5771,44 @@ class AXVoiceRecorderComponent {
5372
5771
  const uploadSignal = this.uploadAbortController.signal;
5373
5772
  this.isUploading.set(true);
5374
5773
  this.uploadProgress.set(0);
5375
- try {
5376
- const command = {
5377
- conversationId: conversation.id,
5774
+ const command = {
5775
+ conversationId: conversation.id,
5776
+ type: 'voice',
5777
+ payload: {
5378
5778
  type: 'voice',
5379
- payload: {
5380
- type: 'voice',
5381
- url: '',
5382
- duration: this.recordingDuration(),
5383
- size: audioBlob.size,
5384
- mimeType: 'audio/webm',
5385
- },
5386
- };
5387
- await this.composerService.sendMessage(command, {
5388
- media: {
5389
- source: audioBlob,
5390
- fileName: `voice-${Date.now()}.webm`,
5391
- mimeType: 'audio/webm',
5392
- onProgress: (percent) => {
5393
- if (!uploadSignal.aborted) {
5394
- this.uploadProgress.set(percent);
5395
- }
5396
- },
5397
- signal: uploadSignal,
5779
+ url: '',
5780
+ duration: this.recordingDuration(),
5781
+ size: audioBlob.size,
5782
+ mimeType: 'audio/webm',
5783
+ },
5784
+ };
5785
+ this.composerService.hideComponent();
5786
+ void this.composerService
5787
+ .sendMessage(command, {
5788
+ media: {
5789
+ source: audioBlob,
5790
+ fileName: `voice-${Date.now()}.webm`,
5791
+ mimeType: 'audio/webm',
5792
+ onProgress: (percent) => {
5793
+ if (!uploadSignal.aborted) {
5794
+ this.uploadProgress.set(percent);
5795
+ }
5398
5796
  },
5399
- });
5400
- }
5401
- catch (error) {
5797
+ signal: uploadSignal,
5798
+ },
5799
+ })
5800
+ .catch((error) => {
5402
5801
  if (!isUploadAborted(error, uploadSignal)) {
5403
5802
  console.error('Failed to send voice message:', error);
5404
5803
  }
5405
- }
5406
- finally {
5804
+ })
5805
+ .finally(() => {
5407
5806
  this.uploadAbortController = undefined;
5408
5807
  if (!uploadSignal.aborted) {
5409
5808
  this.isUploading.set(false);
5410
5809
  this.uploadProgress.set(0);
5411
5810
  }
5412
- }
5811
+ });
5413
5812
  }
5414
5813
  stopAndCleanup() {
5415
5814
  this.abortInProgressUploads();
@@ -6000,282 +6399,67 @@ class AXBaseRegistry {
6000
6399
  update(id, updates) {
6001
6400
  this._items.update((items) => items.map((item) => (item.id === id ? { ...item, ...updates } : item)));
6002
6401
  }
6003
- /**
6004
- * Clear all items
6005
- */
6006
- clear() {
6007
- this._items.set([]);
6008
- }
6009
- /**
6010
- * Get count of registered items
6011
- */
6012
- get count() {
6013
- return this._items().length;
6014
- }
6015
- /**
6016
- * Get count of enabled items
6017
- */
6018
- get enabledCount() {
6019
- return this.enabledItems().length;
6020
- }
6021
- /**
6022
- * Validate item before registration
6023
- * Override in child classes for custom validation
6024
- */
6025
- validateItem(item) {
6026
- if (!item.id) {
6027
- throw new Error(`${this.getRegistryName()}: Item must have an id`);
6028
- }
6029
- }
6030
- }
6031
-
6032
- /**
6033
- * Conversation Item Action Registry
6034
- * Manages actions available in the conversation list item context menu
6035
- */
6036
- /**
6037
- * Conversation Item Action Registry Service
6038
- * Manages available actions in the conversation list item context menu
6039
- */
6040
- class AXConversationItemActionRegistry extends AXBaseRegistry {
6041
- constructor() {
6042
- super();
6043
- this.translation = inject(AXTranslationService);
6044
- this.injector = inject(Injector);
6045
- // Register default actions from individual token
6046
- inject(DEFAULT_CONVERSATION_ITEM_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6047
- // Register actions from REGISTRY_CONFIG
6048
- const config = inject(REGISTRY_CONFIG, { optional: true });
6049
- config?.conversationItemActions?.forEach((action) => this.register(action));
6050
- }
6051
- /**
6052
- * Get registry name for logging
6053
- */
6054
- getRegistryName() {
6055
- return 'ConversationItemActionRegistry';
6056
- }
6057
- /**
6058
- * Get actions for a specific conversation
6059
- */
6060
- getActionsForConversation(conversation) {
6061
- const context = { conversation };
6062
- return this.enabledItems()
6063
- .filter((action) => {
6064
- // Filter by visibility
6065
- if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6066
- return false;
6067
- }
6068
- return true;
6069
- })
6070
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6071
- }
6072
- isActionEnabled(action, context) {
6073
- if (action.enabled === false) {
6074
- return false;
6075
- }
6076
- if (action.enabledWhen) {
6077
- return runInInjectionContext(this.injector, () => action.enabledWhen?.(context)) !== false;
6078
- }
6079
- return true;
6080
- }
6081
- /**
6082
- * Get label for an action
6083
- */
6084
- getActionLabel(action, conversation) {
6085
- if (typeof action.label === 'function') {
6086
- const labelFactory = action.label;
6087
- return this.translation.translateSync(runInInjectionContext(this.injector, () => labelFactory({ conversation })));
6088
- }
6089
- return this.translation.translateSync(action.label);
6090
- }
6091
- /**
6092
- * Get icon for an action
6093
- */
6094
- getActionIcon(action, conversation) {
6095
- if (!action.icon)
6096
- return undefined;
6097
- if (typeof action.icon === 'function') {
6098
- const iconFactory = action.icon;
6099
- return runInInjectionContext(this.injector, () => iconFactory({ conversation }));
6100
- }
6101
- return action.icon;
6102
- }
6103
- /**
6104
- * Get tooltip for an action
6105
- */
6106
- getActionTooltip(action, conversation) {
6107
- if (!action.tooltip)
6108
- return undefined;
6109
- if (typeof action.tooltip === 'function') {
6110
- const tooltipFactory = action.tooltip;
6111
- return this.translation.translateSync(runInInjectionContext(this.injector, () => tooltipFactory({ conversation })));
6112
- }
6113
- return this.translation.translateSync(action.tooltip);
6114
- }
6115
- async executeAction(actionId, context) {
6116
- const action = this.getById(actionId);
6117
- if (!action) {
6118
- throw new Error(`Action "${actionId}" not found`);
6119
- }
6120
- if (!this.isActionEnabled(action, context)) {
6121
- throw new Error(`Action "${actionId}" is not enabled`);
6122
- }
6123
- if (action.handler) {
6124
- await runInInjectionContext(this.injector, () => action.handler?.(context));
6125
- }
6126
- }
6127
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationItemActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6128
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationItemActionRegistry }); }
6129
- }
6130
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationItemActionRegistry, decorators: [{
6131
- type: Injectable
6132
- }], ctorParameters: () => [] });
6133
-
6134
- /**
6135
- * Conversation Tab Registry
6136
- * Register custom tabs for filtering conversations in the sidebar
6137
- */
6138
- /**
6139
- * Conversation Tab Registry Service
6140
- */
6141
- class AXConversationTabRegistry {
6142
- constructor() {
6143
- this._tabs = signal([], ...(ngDevMode ? [{ debugName: "_tabs" }] : /* istanbul ignore next */ []));
6144
- this.translation = inject(AXTranslationService);
6145
- /** All registered tabs */
6146
- this.tabs = this._tabs.asReadonly();
6147
- /** Enabled tabs sorted by priority */
6148
- this.enabledTabs = computed(() => [...this._tabs()].filter((tab) => tab.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledTabs" }] : /* istanbul ignore next */ []));
6149
- /** Default active tab */
6150
- this.defaultTab = computed(() => this.enabledTabs().find((tab) => tab.default === true) || this.enabledTabs()[0], ...(ngDevMode ? [{ debugName: "defaultTab" }] : /* istanbul ignore next */ []));
6151
- // Register default tabs from individual token
6152
- inject(DEFAULT_CONVERSATION_TABS, { optional: true })?.forEach((tab) => this.register(tab));
6153
- // Register tabs from REGISTRY_CONFIG
6154
- const config = inject(REGISTRY_CONFIG, { optional: true });
6155
- config?.conversationTabs?.forEach((tab) => this.register(tab));
6156
- }
6157
- /**
6158
- * Register a conversation tab
6159
- */
6160
- register(tab) {
6161
- // Validate tab
6162
- if (!tab.id) {
6163
- throw new Error('Conversation tab must have an id');
6164
- }
6165
- if (!tab.label) {
6166
- throw new Error('Conversation tab must have a label');
6167
- }
6168
- if (!tab.filter) {
6169
- throw new Error('Conversation tab must have a filter function');
6170
- }
6171
- // Check for duplicates
6172
- const existing = this._tabs().find((t) => t.id === tab.id);
6173
- if (existing) {
6174
- console.warn(`Conversation tab "${tab.id}" is already registered. Replacing...`);
6175
- this.unregister(tab.id);
6176
- }
6177
- // Add to registry
6178
- this._tabs.update((tabs) => [...tabs, tab]);
6179
- }
6180
- /**
6181
- * Unregister a conversation tab
6182
- */
6183
- unregister(id) {
6184
- this._tabs.update((tabs) => tabs.filter((t) => t.id !== id));
6185
- }
6186
- /**
6187
- * Get tab by ID
6188
- */
6189
- getTabById(id) {
6190
- return this._tabs().find((t) => t.id === id);
6191
- }
6192
- getTabLabel(tab) {
6193
- return this.translation.translateSync(tab.label);
6194
- }
6195
- /**
6196
- * Apply tab filter to conversations
6197
- */
6198
- filterConversations(tabId, conversations) {
6199
- const tab = this.getTabById(tabId);
6200
- if (!tab) {
6201
- throw new Error(`Tab "${tabId}" not found`);
6202
- }
6203
- return tab.filter(conversations);
6204
- }
6205
- /**
6206
- * Get badge for a tab
6207
- */
6208
- getBadge(tabId, conversations) {
6209
- const tab = this.getTabById(tabId);
6210
- if (!tab || !tab.badge) {
6211
- return undefined;
6212
- }
6213
- return tab.badge(conversations);
6402
+ /**
6403
+ * Clear all items
6404
+ */
6405
+ clear() {
6406
+ this._items.set([]);
6214
6407
  }
6215
6408
  /**
6216
- * Enable/disable a tab
6409
+ * Get count of registered items
6217
6410
  */
6218
- setEnabled(id, enabled) {
6219
- this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, enabled } : tab)));
6411
+ get count() {
6412
+ return this._items().length;
6220
6413
  }
6221
6414
  /**
6222
- * Set default tab
6415
+ * Get count of enabled items
6223
6416
  */
6224
- setDefault(id) {
6225
- this._tabs.update((tabs) => tabs.map((tab) => ({
6226
- ...tab,
6227
- default: tab.id === id,
6228
- })));
6417
+ get enabledCount() {
6418
+ return this.enabledItems().length;
6229
6419
  }
6230
6420
  /**
6231
- * Clear all tabs
6421
+ * Validate item before registration
6422
+ * Override in child classes for custom validation
6232
6423
  */
6233
- clear() {
6234
- this._tabs.set([]);
6424
+ validateItem(item) {
6425
+ if (!item.id) {
6426
+ throw new Error(`${this.getRegistryName()}: Item must have an id`);
6427
+ }
6235
6428
  }
6236
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationTabRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6237
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationTabRegistry }); }
6238
6429
  }
6239
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationTabRegistry, decorators: [{
6240
- type: Injectable
6241
- }], ctorParameters: () => [] });
6242
6430
 
6243
6431
  /**
6244
- * Info Bar Action Registry
6245
- * Manages actions available in the conversation info bar
6432
+ * Conversation Item Action Registry
6433
+ * Manages actions available in the conversation list item context menu
6246
6434
  */
6247
6435
  /**
6248
- * Info Bar Action Registry Service
6249
- * Manages available actions in the conversation info bar
6436
+ * Conversation Item Action Registry Service
6437
+ * Manages available actions in the conversation list item context menu
6250
6438
  */
6251
- class AXInfoBarActionRegistry extends AXBaseRegistry {
6439
+ class AXConversationItemActionRegistry extends AXBaseRegistry {
6252
6440
  constructor() {
6253
6441
  super();
6254
6442
  this.translation = inject(AXTranslationService);
6255
6443
  this.injector = inject(Injector);
6256
6444
  // Register default actions from individual token
6257
- inject(DEFAULT_INFO_BAR_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6445
+ inject(DEFAULT_CONVERSATION_ITEM_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6258
6446
  // Register actions from REGISTRY_CONFIG
6259
6447
  const config = inject(REGISTRY_CONFIG, { optional: true });
6260
- config?.infoBarActions?.forEach((action) => this.register(action));
6448
+ config?.conversationItemActions?.forEach((action) => this.register(action));
6261
6449
  }
6262
6450
  /**
6263
6451
  * Get registry name for logging
6264
6452
  */
6265
6453
  getRegistryName() {
6266
- return 'InfoBarActionRegistry';
6454
+ return 'ConversationItemActionRegistry';
6267
6455
  }
6268
6456
  /**
6269
6457
  * Get actions for a specific conversation
6270
6458
  */
6271
- getActionsForConversation(conversation, location) {
6459
+ getActionsForConversation(conversation) {
6272
6460
  const context = { conversation };
6273
6461
  return this.enabledItems()
6274
6462
  .filter((action) => {
6275
- // Filter by location if specified
6276
- if (location && action.location && action.location !== location) {
6277
- return false;
6278
- }
6279
6463
  // Filter by visibility
6280
6464
  if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6281
6465
  return false;
@@ -6284,18 +6468,6 @@ class AXInfoBarActionRegistry extends AXBaseRegistry {
6284
6468
  })
6285
6469
  .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6286
6470
  }
6287
- /**
6288
- * Get inline actions (toolbar buttons)
6289
- */
6290
- getInlineActions(conversation) {
6291
- return this.getActionsForConversation(conversation, 'inline');
6292
- }
6293
- /**
6294
- * Get menu actions (dropdown items)
6295
- */
6296
- getMenuActions(conversation) {
6297
- return this.getActionsForConversation(conversation, 'menu');
6298
- }
6299
6471
  isActionEnabled(action, context) {
6300
6472
  if (action.enabled === false) {
6301
6473
  return false;
@@ -6339,653 +6511,575 @@ class AXInfoBarActionRegistry extends AXBaseRegistry {
6339
6511
  }
6340
6512
  return this.translation.translateSync(action.tooltip);
6341
6513
  }
6342
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXInfoBarActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6343
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXInfoBarActionRegistry }); }
6344
- }
6345
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXInfoBarActionRegistry, decorators: [{
6346
- type: Injectable
6347
- }], ctorParameters: () => [] });
6348
-
6349
- /**
6350
- * Message Action Registry
6351
- * Register custom actions that can be performed on messages
6352
- */
6353
- /**
6354
- * Message Action Registry Service
6355
- */
6356
- class AXMessageActionRegistry {
6357
- constructor() {
6358
- this._actions = signal([], ...(ngDevMode ? [{ debugName: "_actions" }] : /* istanbul ignore next */ []));
6359
- this.translation = inject(AXTranslationService);
6360
- this.injector = inject(Injector);
6361
- /** All registered actions */
6362
- this.actions = this._actions.asReadonly();
6363
- /** Actions sorted by priority */
6364
- this.sortedActions = computed(() => [...this._actions()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "sortedActions" }] : /* istanbul ignore next */ []));
6365
- // Register default actions from individual token
6366
- const defaultActions = inject(DEFAULT_MESSAGE_ACTIONS, { optional: true });
6367
- if (defaultActions) {
6368
- defaultActions.forEach((action) => this.register(action));
6369
- }
6370
- // Register actions from REGISTRY_CONFIG
6371
- const config = inject(REGISTRY_CONFIG, { optional: true });
6372
- config?.messageActions?.forEach((action) => this.register(action));
6373
- }
6374
- /**
6375
- * Register a message action
6376
- */
6377
- register(action) {
6378
- // Validate action
6379
- if (!action.id) {
6380
- throw new Error('Message action must have an id');
6381
- }
6382
- if (!action.label) {
6383
- throw new Error('Message action must have a label');
6384
- }
6385
- if (!action.handler) {
6386
- throw new Error('Message action must have a handler');
6387
- }
6388
- // Check for duplicates
6389
- const existing = this._actions().find((a) => a.id === action.id);
6390
- if (existing) {
6391
- console.warn(`Message action "${action.id}" is already registered. Replacing...`);
6392
- this.unregister(action.id);
6393
- }
6394
- // Add to registry
6395
- this._actions.update((actions) => [...actions, action]);
6396
- }
6397
- /**
6398
- * Unregister a message action
6399
- */
6400
- unregister(id) {
6401
- this._actions.update((actions) => actions.filter((a) => a.id !== id));
6402
- }
6403
- /**
6404
- * Get all actions for a specific message
6405
- */
6406
- getActions(message, currentUser) {
6407
- return this.sortedActions().filter((action) => runInInjectionContext(this.injector, () => action.visible(message, currentUser)) &&
6408
- runInInjectionContext(this.injector, () => action.enabled(message, currentUser)));
6409
- }
6410
- /**
6411
- * Get menu actions for a message
6412
- */
6413
- getMenuActions(message, currentUser) {
6414
- return this.getActions(message, currentUser);
6415
- }
6416
- /** Whether an action is enabled (safe to call outside an injection context). */
6417
- isActionEnabled(action, message, currentUser) {
6418
- return runInInjectionContext(this.injector, () => action.enabled(message, currentUser));
6419
- }
6420
- getActionLabel(action) {
6421
- return this.translation.translateSync(action.label);
6422
- }
6423
- /**
6424
- * Get inline actions for a message
6425
- */
6426
- getInlineActions(message, currentUser) {
6427
- return this.getActions(message, currentUser).filter((action) => action.showInline === true);
6428
- }
6429
- /**
6430
- * Get action by ID
6431
- */
6432
- getActionById(id) {
6433
- return this._actions().find((a) => a.id === id);
6434
- }
6435
- /**
6436
- * Execute an action
6437
- */
6438
6514
  async executeAction(actionId, context) {
6439
- const action = this.getActionById(actionId);
6515
+ const action = this.getById(actionId);
6440
6516
  if (!action) {
6441
6517
  throw new Error(`Action "${actionId}" not found`);
6442
6518
  }
6443
- // Check if enabled
6444
- if (!runInInjectionContext(this.injector, () => action.enabled(context.message, context.currentUser))) {
6519
+ if (!this.isActionEnabled(action, context)) {
6445
6520
  throw new Error(`Action "${actionId}" is not enabled`);
6446
6521
  }
6447
- // Execute handler
6448
- await runInInjectionContext(this.injector, () => action.handler(context));
6449
- }
6450
- /**
6451
- * Clear all actions
6452
- */
6453
- clear() {
6454
- this._actions.set([]);
6522
+ if (action.handler) {
6523
+ await runInInjectionContext(this.injector, () => action.handler?.(context));
6524
+ }
6455
6525
  }
6456
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMessageActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6457
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMessageActionRegistry }); }
6526
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationItemActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6527
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationItemActionRegistry }); }
6458
6528
  }
6459
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMessageActionRegistry, decorators: [{
6529
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationItemActionRegistry, decorators: [{
6460
6530
  type: Injectable
6461
6531
  }], ctorParameters: () => [] });
6462
6532
 
6463
6533
  /**
6464
- * IndexedDB Storage Wrapper
6465
- * Provides persistent storage for conversation data
6534
+ * Conversation Tab Registry
6535
+ * Register custom tabs for filtering conversations in the sidebar
6466
6536
  */
6467
- const DB_NAME = 'acorex-conversation-db';
6468
- const DB_VERSION = 2;
6469
- // Store names
6470
- const AXConversationIndexedDbStores = {
6471
- PARTICIPANTS: 'participants',
6472
- CONVERSATIONS: 'conversations',
6473
- MESSAGES: 'messages',
6474
- MEDIA: 'media',
6475
- SETTINGS: 'settings',
6476
- };
6477
- class AXConversationIndexedDbStorage {
6537
+ /**
6538
+ * Conversation Tab Registry Service
6539
+ */
6540
+ class AXConversationTabRegistry {
6478
6541
  constructor() {
6479
- this.db = null;
6480
- this.initPromise = null;
6481
- }
6482
- /**
6483
- * Initialize IndexedDB
6484
- */
6485
- async init() {
6486
- if (this.db)
6487
- return;
6488
- if (this.initPromise)
6489
- return this.initPromise;
6490
- this.initPromise = new Promise((resolve, reject) => {
6491
- const request = indexedDB.open(DB_NAME, DB_VERSION);
6492
- request.onerror = () => reject(request.error);
6493
- request.onsuccess = () => {
6494
- this.db = request.result;
6495
- resolve();
6496
- };
6497
- request.onupgradeneeded = (event) => {
6498
- const db = event.target.result;
6499
- // Create object stores if they don't exist
6500
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.PARTICIPANTS)) {
6501
- db.createObjectStore(AXConversationIndexedDbStores.PARTICIPANTS, { keyPath: 'id' });
6502
- }
6503
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.CONVERSATIONS)) {
6504
- const convStore = db.createObjectStore(AXConversationIndexedDbStores.CONVERSATIONS, { keyPath: 'id' });
6505
- convStore.createIndex('lastMessageAt', 'lastMessageAt', { unique: false });
6506
- }
6507
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MESSAGES)) {
6508
- const msgStore = db.createObjectStore(AXConversationIndexedDbStores.MESSAGES, { keyPath: 'id' });
6509
- msgStore.createIndex('conversationId', 'conversationId', { unique: false });
6510
- msgStore.createIndex('timestamp', 'timestamp', { unique: false });
6511
- }
6512
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.SETTINGS)) {
6513
- db.createObjectStore(AXConversationIndexedDbStores.SETTINGS, { keyPath: 'key' });
6514
- }
6515
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MEDIA)) {
6516
- db.createObjectStore(AXConversationIndexedDbStores.MEDIA, { keyPath: 'id' });
6517
- }
6518
- };
6519
- });
6520
- return this.initPromise;
6542
+ this._tabs = signal([], ...(ngDevMode ? [{ debugName: "_tabs" }] : /* istanbul ignore next */ []));
6543
+ this.translation = inject(AXTranslationService);
6544
+ /** All registered tabs */
6545
+ this.tabs = this._tabs.asReadonly();
6546
+ /** Enabled tabs sorted by priority */
6547
+ this.enabledTabs = computed(() => [...this._tabs()].filter((tab) => tab.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledTabs" }] : /* istanbul ignore next */ []));
6548
+ /** Default active tab */
6549
+ this.defaultTab = computed(() => this.enabledTabs().find((tab) => tab.default === true) || this.enabledTabs()[0], ...(ngDevMode ? [{ debugName: "defaultTab" }] : /* istanbul ignore next */ []));
6550
+ // Register default tabs from individual token
6551
+ inject(DEFAULT_CONVERSATION_TABS, { optional: true })?.forEach((tab) => this.register(tab));
6552
+ // Register tabs from REGISTRY_CONFIG
6553
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6554
+ config?.conversationTabs?.forEach((tab) => this.register(tab));
6521
6555
  }
6522
6556
  /**
6523
- * Get a value from a store
6557
+ * Register a conversation tab
6524
6558
  */
6525
- async get(storeName, key) {
6526
- await this.init();
6527
- if (!this.db)
6528
- throw new Error('Database not initialized');
6529
- const db = this.db;
6530
- return new Promise((resolve, reject) => {
6531
- const transaction = db.transaction(storeName, 'readonly');
6532
- const store = transaction.objectStore(storeName);
6533
- const request = store.get(key);
6534
- request.onsuccess = () => resolve(request.result);
6535
- request.onerror = () => reject(request.error);
6536
- });
6559
+ register(tab) {
6560
+ // Validate tab
6561
+ if (!tab.id) {
6562
+ throw new Error('Conversation tab must have an id');
6563
+ }
6564
+ if (!tab.label) {
6565
+ throw new Error('Conversation tab must have a label');
6566
+ }
6567
+ if (!tab.filter) {
6568
+ throw new Error('Conversation tab must have a filter function');
6569
+ }
6570
+ // Check for duplicates
6571
+ const existing = this._tabs().find((t) => t.id === tab.id);
6572
+ if (existing) {
6573
+ console.warn(`Conversation tab "${tab.id}" is already registered. Replacing...`);
6574
+ this.unregister(tab.id);
6575
+ }
6576
+ // Add to registry
6577
+ this._tabs.update((tabs) => [...tabs, tab]);
6537
6578
  }
6538
6579
  /**
6539
- * Get all values from a store
6580
+ * Unregister a conversation tab
6540
6581
  */
6541
- async getAll(storeName) {
6542
- await this.init();
6543
- if (!this.db)
6544
- throw new Error('Database not initialized');
6545
- const db = this.db;
6546
- return new Promise((resolve, reject) => {
6547
- const transaction = db.transaction(storeName, 'readonly');
6548
- const store = transaction.objectStore(storeName);
6549
- const request = store.getAll();
6550
- request.onsuccess = () => resolve(request.result);
6551
- request.onerror = () => reject(request.error);
6552
- });
6582
+ unregister(id) {
6583
+ this._tabs.update((tabs) => tabs.filter((t) => t.id !== id));
6553
6584
  }
6554
6585
  /**
6555
- * Get values by index
6586
+ * Get tab by ID
6556
6587
  */
6557
- async getAllByIndex(storeName, indexName, query) {
6558
- await this.init();
6559
- if (!this.db)
6560
- throw new Error('Database not initialized');
6561
- const db = this.db;
6562
- return new Promise((resolve, reject) => {
6563
- const transaction = db.transaction(storeName, 'readonly');
6564
- const store = transaction.objectStore(storeName);
6565
- const index = store.index(indexName);
6566
- const request = index.getAll(query);
6567
- request.onsuccess = () => resolve(request.result);
6568
- request.onerror = () => reject(request.error);
6569
- });
6588
+ getTabById(id) {
6589
+ return this._tabs().find((t) => t.id === id);
6590
+ }
6591
+ getTabLabel(tab) {
6592
+ return this.translation.translateSync(tab.label);
6570
6593
  }
6571
6594
  /**
6572
- * Put a value into a store
6595
+ * Apply tab filter to conversations
6573
6596
  */
6574
- async put(storeName, value) {
6575
- await this.init();
6576
- if (!this.db)
6577
- throw new Error('Database not initialized');
6578
- const db = this.db;
6579
- return new Promise((resolve, reject) => {
6580
- const transaction = db.transaction(storeName, 'readwrite');
6581
- const store = transaction.objectStore(storeName);
6582
- const request = store.put(value);
6583
- request.onsuccess = () => resolve();
6584
- request.onerror = () => reject(request.error);
6585
- });
6597
+ filterConversations(tabId, conversations) {
6598
+ const tab = this.getTabById(tabId);
6599
+ if (!tab) {
6600
+ throw new Error(`Tab "${tabId}" not found`);
6601
+ }
6602
+ return tab.filter(conversations);
6586
6603
  }
6587
6604
  /**
6588
- * Delete a value from a store
6605
+ * Get badge for a tab
6589
6606
  */
6590
- async delete(storeName, key) {
6591
- await this.init();
6592
- if (!this.db)
6593
- throw new Error('Database not initialized');
6594
- const db = this.db;
6595
- return new Promise((resolve, reject) => {
6596
- const transaction = db.transaction(storeName, 'readwrite');
6597
- const store = transaction.objectStore(storeName);
6598
- const request = store.delete(key);
6599
- request.onsuccess = () => resolve();
6600
- request.onerror = () => reject(request.error);
6601
- });
6607
+ getBadge(tabId, conversations) {
6608
+ const tab = this.getTabById(tabId);
6609
+ if (!tab || !tab.badge) {
6610
+ return undefined;
6611
+ }
6612
+ return tab.badge(conversations);
6602
6613
  }
6603
6614
  /**
6604
- * Clear all data from a store
6615
+ * Enable/disable a tab
6605
6616
  */
6606
- async clear(storeName) {
6607
- await this.init();
6608
- if (!this.db)
6609
- throw new Error('Database not initialized');
6610
- const db = this.db;
6611
- return new Promise((resolve, reject) => {
6612
- const transaction = db.transaction(storeName, 'readwrite');
6613
- const store = transaction.objectStore(storeName);
6614
- const request = store.clear();
6615
- request.onsuccess = () => resolve();
6616
- request.onerror = () => reject(request.error);
6617
- });
6618
- }
6619
- // =====================
6620
- // Convenience Methods
6621
- // =====================
6622
- async getParticipant(id) {
6623
- return this.get(AXConversationIndexedDbStores.PARTICIPANTS, id);
6624
- }
6625
- async getAllParticipants() {
6626
- return this.getAll(AXConversationIndexedDbStores.PARTICIPANTS);
6627
- }
6628
- async putParticipant(participant) {
6629
- return this.put(AXConversationIndexedDbStores.PARTICIPANTS, participant);
6630
- }
6631
- async getConversation(id) {
6632
- return this.get(AXConversationIndexedDbStores.CONVERSATIONS, id);
6633
- }
6634
- async getAllConversations() {
6635
- return this.getAll(AXConversationIndexedDbStores.CONVERSATIONS);
6636
- }
6637
- async putConversation(conversation) {
6638
- return this.put(AXConversationIndexedDbStores.CONVERSATIONS, conversation);
6639
- }
6640
- async deleteConversation(id) {
6641
- return this.delete(AXConversationIndexedDbStores.CONVERSATIONS, id);
6642
- }
6643
- async getMessage(id) {
6644
- return this.get(AXConversationIndexedDbStores.MESSAGES, id);
6645
- }
6646
- async getAllMessages() {
6647
- return this.getAll(AXConversationIndexedDbStores.MESSAGES);
6648
- }
6649
- async getMessagesByConversation(conversationId) {
6650
- return this.getAllByIndex(AXConversationIndexedDbStores.MESSAGES, 'conversationId', conversationId);
6651
- }
6652
- async putMessage(message) {
6653
- return this.put(AXConversationIndexedDbStores.MESSAGES, message);
6654
- }
6655
- async deleteMessage(id) {
6656
- return this.delete(AXConversationIndexedDbStores.MESSAGES, id);
6617
+ setEnabled(id, enabled) {
6618
+ this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, enabled } : tab)));
6657
6619
  }
6658
- async getSetting(key) {
6659
- const result = await this.get(AXConversationIndexedDbStores.SETTINGS, key);
6660
- return result?.value;
6620
+ /**
6621
+ * Set default tab
6622
+ */
6623
+ setDefault(id) {
6624
+ this._tabs.update((tabs) => tabs.map((tab) => ({
6625
+ ...tab,
6626
+ default: tab.id === id,
6627
+ })));
6661
6628
  }
6662
- async putSetting(key, value) {
6663
- return this.put(AXConversationIndexedDbStores.SETTINGS, { key, value });
6629
+ /**
6630
+ * Clear all tabs
6631
+ */
6632
+ clear() {
6633
+ this._tabs.set([]);
6664
6634
  }
6665
- async getMedia(id) {
6666
- return this.get(AXConversationIndexedDbStores.MEDIA, id);
6635
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationTabRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6636
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationTabRegistry }); }
6637
+ }
6638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationTabRegistry, decorators: [{
6639
+ type: Injectable
6640
+ }], ctorParameters: () => [] });
6641
+
6642
+ /**
6643
+ * Info Bar Action Registry
6644
+ * Manages actions available in the conversation info bar
6645
+ */
6646
+ /**
6647
+ * Info Bar Action Registry Service
6648
+ * Manages available actions in the conversation info bar
6649
+ */
6650
+ class AXInfoBarActionRegistry extends AXBaseRegistry {
6651
+ constructor() {
6652
+ super();
6653
+ this.translation = inject(AXTranslationService);
6654
+ this.injector = inject(Injector);
6655
+ // Register default actions from individual token
6656
+ inject(DEFAULT_INFO_BAR_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6657
+ // Register actions from REGISTRY_CONFIG
6658
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6659
+ config?.infoBarActions?.forEach((action) => this.register(action));
6667
6660
  }
6668
- async putMedia(record) {
6669
- return this.put(AXConversationIndexedDbStores.MEDIA, record);
6661
+ /**
6662
+ * Get registry name for logging
6663
+ */
6664
+ getRegistryName() {
6665
+ return 'InfoBarActionRegistry';
6670
6666
  }
6671
- async deleteMedia(id) {
6672
- return this.delete(AXConversationIndexedDbStores.MEDIA, id);
6667
+ /**
6668
+ * Get actions for a specific conversation
6669
+ */
6670
+ getActionsForConversation(conversation, location) {
6671
+ const context = { conversation };
6672
+ return this.enabledItems()
6673
+ .filter((action) => {
6674
+ // Filter by location if specified
6675
+ if (location && action.location && action.location !== location) {
6676
+ return false;
6677
+ }
6678
+ // Filter by visibility
6679
+ if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6680
+ return false;
6681
+ }
6682
+ return true;
6683
+ })
6684
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6685
+ }
6686
+ /**
6687
+ * Get inline actions (toolbar buttons)
6688
+ */
6689
+ getInlineActions(conversation) {
6690
+ return this.getActionsForConversation(conversation, 'inline');
6673
6691
  }
6674
- }
6675
- // Export singleton instance
6676
- const axConversationIndexedDbStorage = new AXConversationIndexedDbStorage();
6677
-
6678
- var indexeddbStorage = /*#__PURE__*/Object.freeze({
6679
- __proto__: null,
6680
- AXConversationIndexedDbStorage: AXConversationIndexedDbStorage,
6681
- AXConversationIndexedDbStores: AXConversationIndexedDbStores,
6682
- axConversationIndexedDbStorage: axConversationIndexedDbStorage
6683
- });
6684
-
6685
- class AXConversationMessageUtilsService {
6686
6692
  /**
6687
- * Normalize optional avatar/icon values so empty or whitespace-only strings
6688
- * are treated as missing data.
6693
+ * Get menu actions (dropdown items)
6689
6694
  */
6690
- static normalizeOptionalMediaValue(value) {
6691
- if (typeof value !== 'string') {
6692
- return undefined;
6695
+ getMenuActions(conversation) {
6696
+ return this.getActionsForConversation(conversation, 'menu');
6697
+ }
6698
+ isActionEnabled(action, context) {
6699
+ if (action.enabled === false) {
6700
+ return false;
6693
6701
  }
6694
- const normalized = value.trim();
6695
- return normalized.length > 0 ? normalized : undefined;
6702
+ if (action.enabledWhen) {
6703
+ return runInInjectionContext(this.injector, () => action.enabledWhen?.(context)) !== false;
6704
+ }
6705
+ return true;
6696
6706
  }
6697
6707
  /**
6698
- * Get conversation avatar image URL.
6708
+ * Get label for an action
6699
6709
  */
6700
- static getConversationAvatar(conversation) {
6701
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.avatar);
6710
+ getActionLabel(action, conversation) {
6711
+ if (typeof action.label === 'function') {
6712
+ const labelFactory = action.label;
6713
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => labelFactory({ conversation })));
6714
+ }
6715
+ return this.translation.translateSync(action.label);
6702
6716
  }
6703
6717
  /**
6704
- * Font Awesome icon class(es) for a conversation when there is no avatar image.
6718
+ * Get icon for an action
6705
6719
  */
6706
- static getConversationAvatarIcon(conversation) {
6707
- if (AXConversationMessageUtilsService.getConversationAvatar(conversation)) {
6720
+ getActionIcon(action, conversation) {
6721
+ if (!action.icon)
6708
6722
  return undefined;
6723
+ if (typeof action.icon === 'function') {
6724
+ const iconFactory = action.icon;
6725
+ return runInInjectionContext(this.injector, () => iconFactory({ conversation }));
6709
6726
  }
6710
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.icon);
6727
+ return action.icon;
6711
6728
  }
6712
6729
  /**
6713
- * Get sender name from message
6730
+ * Get tooltip for an action
6714
6731
  */
6715
- static getSenderName(message, conversation) {
6716
- const participant = conversation.participants.find((p) => p.id === message.senderId);
6717
- return participant?.name || translateSync('@acorex:chat.fallbacks.unknown-user');
6732
+ getActionTooltip(action, conversation) {
6733
+ if (!action.tooltip)
6734
+ return undefined;
6735
+ if (typeof action.tooltip === 'function') {
6736
+ const tooltipFactory = action.tooltip;
6737
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => tooltipFactory({ conversation })));
6738
+ }
6739
+ return this.translation.translateSync(action.tooltip);
6740
+ }
6741
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXInfoBarActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6742
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXInfoBarActionRegistry }); }
6743
+ }
6744
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXInfoBarActionRegistry, decorators: [{
6745
+ type: Injectable
6746
+ }], ctorParameters: () => [] });
6747
+
6748
+ /**
6749
+ * Message Action Registry
6750
+ * Register custom actions that can be performed on messages
6751
+ */
6752
+ /**
6753
+ * Message Action Registry Service
6754
+ */
6755
+ class AXMessageActionRegistry {
6756
+ constructor() {
6757
+ this._actions = signal([], ...(ngDevMode ? [{ debugName: "_actions" }] : /* istanbul ignore next */ []));
6758
+ this.translation = inject(AXTranslationService);
6759
+ this.injector = inject(Injector);
6760
+ /** All registered actions */
6761
+ this.actions = this._actions.asReadonly();
6762
+ /** Actions sorted by priority */
6763
+ this.sortedActions = computed(() => [...this._actions()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "sortedActions" }] : /* istanbul ignore next */ []));
6764
+ // Register default actions from individual token
6765
+ const defaultActions = inject(DEFAULT_MESSAGE_ACTIONS, { optional: true });
6766
+ if (defaultActions) {
6767
+ defaultActions.forEach((action) => this.register(action));
6768
+ }
6769
+ // Register actions from REGISTRY_CONFIG
6770
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6771
+ config?.messageActions?.forEach((action) => this.register(action));
6718
6772
  }
6719
6773
  /**
6720
- * Get sender avatar image URL (takes precedence over {@link getSenderAvatarIcon}).
6774
+ * Register a message action
6721
6775
  */
6722
- static getSenderAvatar(message, conversation) {
6723
- const participant = conversation.participants.find((p) => p.id === message.senderId);
6724
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar);
6776
+ register(action) {
6777
+ // Validate action
6778
+ if (!action.id) {
6779
+ throw new Error('Message action must have an id');
6780
+ }
6781
+ if (!action.label) {
6782
+ throw new Error('Message action must have a label');
6783
+ }
6784
+ if (!action.handler) {
6785
+ throw new Error('Message action must have a handler');
6786
+ }
6787
+ // Check for duplicates
6788
+ const existing = this._actions().find((a) => a.id === action.id);
6789
+ if (existing) {
6790
+ console.warn(`Message action "${action.id}" is already registered. Replacing...`);
6791
+ this.unregister(action.id);
6792
+ }
6793
+ // Add to registry
6794
+ this._actions.update((actions) => [...actions, action]);
6725
6795
  }
6726
6796
  /**
6727
- * Font Awesome icon class(es) for the sender when there is no avatar image:
6728
- * participant `icon` first, then conversation-level `icon`.
6797
+ * Unregister a message action
6729
6798
  */
6730
- static getSenderAvatarIcon(message, conversation) {
6731
- const participant = conversation.participants.find((p) => p.id === message.senderId);
6732
- if (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar)) {
6733
- return undefined;
6734
- }
6735
- return (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.icon) ??
6736
- AXConversationMessageUtilsService.getConversationAvatarIcon(conversation));
6799
+ unregister(id) {
6800
+ this._actions.update((actions) => actions.filter((a) => a.id !== id));
6737
6801
  }
6738
6802
  /**
6739
- * Get initials from name
6803
+ * Get all actions for a specific message
6740
6804
  */
6741
- static getInitials(name) {
6742
- if (!name)
6743
- return '?';
6744
- return name
6745
- .split(' ')
6746
- .map((n) => n[0])
6747
- .join('')
6748
- .toUpperCase()
6749
- .substring(0, 2);
6805
+ getActions(message, currentUser) {
6806
+ return this.sortedActions().filter((action) => runInInjectionContext(this.injector, () => action.visible(message, currentUser)) &&
6807
+ runInInjectionContext(this.injector, () => action.enabled(message, currentUser)));
6750
6808
  }
6751
6809
  /**
6752
- * Type guard for text payload
6810
+ * Get menu actions for a message
6753
6811
  */
6754
- static isTextPayload(payload) {
6755
- return 'text' in payload && typeof payload.text === 'string';
6812
+ getMenuActions(message, currentUser) {
6813
+ return this.getActions(message, currentUser);
6814
+ }
6815
+ /** Whether an action is enabled (safe to call outside an injection context). */
6816
+ isActionEnabled(action, message, currentUser) {
6817
+ return runInInjectionContext(this.injector, () => action.enabled(message, currentUser));
6818
+ }
6819
+ getActionLabel(action) {
6820
+ return this.translation.translateSync(action.label);
6756
6821
  }
6757
6822
  /**
6758
- * Get message text content
6823
+ * Get inline actions for a message
6759
6824
  */
6760
- static getMessageText(message) {
6761
- if (message.type === 'text' && AXConversationMessageUtilsService.isTextPayload(message.payload)) {
6762
- return message.payload.text;
6763
- }
6764
- return `[${message.type}]`;
6825
+ getInlineActions(message, currentUser) {
6826
+ return this.getActions(message, currentUser).filter((action) => action.showInline === true);
6765
6827
  }
6766
6828
  /**
6767
- * Format message preview text
6829
+ * Get action by ID
6768
6830
  */
6769
- static getPreviewText(message, maxLength = 50) {
6770
- // Handle different message types
6771
- switch (message.type) {
6772
- case 'text': {
6773
- const textPayload = message.payload;
6774
- const text = textPayload.text || '';
6775
- const truncatedText = text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
6776
- return {
6777
- value: truncatedText,
6778
- type: 'text',
6779
- icon: 'fa-light fa-message',
6780
- };
6781
- }
6782
- case 'image': {
6783
- const imagePayload = normalizeImagePayload(message.payload);
6784
- const first = imagePayload.images[0];
6785
- const label = imagePayload.caption?.trim() ||
6786
- (imagePayload.images && imagePayload.images.length > 1
6787
- ? `${imagePayload.images.length} images`
6788
- : '');
6789
- return {
6790
- value: label || first?.thumbnailUrl || first?.url || '',
6791
- type: 'image',
6792
- icon: 'fa-light fa-image',
6793
- };
6794
- }
6795
- case 'video': {
6796
- const videoPayload = normalizeVideoPayload(message.payload);
6797
- const first = videoPayload.videos[0];
6798
- const label = videoPayload.caption?.trim() ||
6799
- (videoPayload.videos.length > 1 ? `${videoPayload.videos.length} videos` : '');
6800
- return {
6801
- value: label || first?.url || '',
6802
- type: 'video',
6803
- icon: 'fa-light fa-video',
6804
- };
6805
- }
6806
- case 'audio': {
6807
- const audioPayload = normalizeAudioPayload(message.payload);
6808
- const names = audioPayload.audios.map((a) => a.title).filter(Boolean).join(', ');
6809
- const first = audioPayload.audios[0];
6810
- const label = audioPayload.caption?.trim() ||
6811
- (audioPayload.audios.length > 1 ? `${audioPayload.audios.length} audio files` : names);
6812
- return {
6813
- value: label || first?.url || '',
6814
- type: 'audio',
6815
- icon: 'fa-light fa-music',
6816
- };
6817
- }
6818
- case 'voice': {
6819
- const voicePayload = message.payload;
6820
- return {
6821
- value: voicePayload.url || '',
6822
- type: 'voice',
6823
- icon: 'fa-light fa-microphone',
6824
- };
6825
- }
6826
- case 'file': {
6827
- const filePayload = normalizeFilePayload(message.payload);
6828
- const names = filePayload.files.map((f) => f.name).join(', ');
6829
- const label = filePayload.caption?.trim() ||
6830
- (filePayload.files.length > 1 ? `${filePayload.files.length} files` : names);
6831
- return {
6832
- value: label || names,
6833
- type: 'file',
6834
- icon: 'fa-light fa-file',
6835
- };
6836
- }
6837
- case 'location': {
6838
- const locationPayload = message.payload;
6839
- return {
6840
- value: locationPayload.latitude && locationPayload.longitude
6841
- ? `${locationPayload.latitude},${locationPayload.longitude}`
6842
- : '',
6843
- type: 'location',
6844
- icon: 'fa-light fa-location-dot',
6845
- };
6846
- }
6847
- case 'sticker': {
6848
- const stickerPayload = message.payload;
6849
- return {
6850
- value: stickerPayload.url || '',
6851
- type: 'sticker',
6852
- icon: 'fa-light fa-face-smile',
6853
- };
6854
- }
6855
- default:
6856
- return {
6857
- value: '',
6858
- type: message.type,
6859
- icon: 'fa-light fa-message',
6860
- };
6831
+ getActionById(id) {
6832
+ return this._actions().find((a) => a.id === id);
6833
+ }
6834
+ /**
6835
+ * Execute an action
6836
+ */
6837
+ async executeAction(actionId, context) {
6838
+ const action = this.getActionById(actionId);
6839
+ if (!action) {
6840
+ throw new Error(`Action "${actionId}" not found`);
6841
+ }
6842
+ // Check if enabled
6843
+ if (!runInInjectionContext(this.injector, () => action.enabled(context.message, context.currentUser))) {
6844
+ throw new Error(`Action "${actionId}" is not enabled`);
6861
6845
  }
6846
+ // Execute handler
6847
+ await runInInjectionContext(this.injector, () => action.handler(context));
6862
6848
  }
6863
6849
  /**
6864
- * Check if message is from current user
6850
+ * Clear all actions
6865
6851
  */
6866
- static isOwnMessage(message, currentUserId) {
6867
- return message.senderId === currentUserId;
6852
+ clear() {
6853
+ this._actions.set([]);
6854
+ }
6855
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMessageActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6856
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMessageActionRegistry }); }
6857
+ }
6858
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMessageActionRegistry, decorators: [{
6859
+ type: Injectable
6860
+ }], ctorParameters: () => [] });
6861
+
6862
+ /**
6863
+ * IndexedDB Storage Wrapper
6864
+ * Provides persistent storage for conversation data
6865
+ */
6866
+ const DB_NAME = 'acorex-conversation-db';
6867
+ const DB_VERSION = 2;
6868
+ // Store names
6869
+ const AXConversationIndexedDbStores = {
6870
+ PARTICIPANTS: 'participants',
6871
+ CONVERSATIONS: 'conversations',
6872
+ MESSAGES: 'messages',
6873
+ MEDIA: 'media',
6874
+ SETTINGS: 'settings',
6875
+ };
6876
+ class AXConversationIndexedDbStorage {
6877
+ constructor() {
6878
+ this.db = null;
6879
+ this.initPromise = null;
6868
6880
  }
6869
6881
  /**
6870
- * Get message status icon class (Font Awesome)
6882
+ * Initialize IndexedDB
6871
6883
  */
6872
- static getStatusIcon(message) {
6873
- switch (message.status) {
6874
- case 'sending':
6875
- return 'fa-light fa-clock';
6876
- case 'sent':
6877
- return 'fa-light fa-check';
6878
- case 'delivered':
6879
- return 'fa-light fa-check-double';
6880
- case 'read':
6881
- return 'fa-light fa-check-double';
6882
- case 'failed':
6883
- return 'fa-light fa-circle-exclamation';
6884
- default:
6885
- return 'fa-light fa-check';
6886
- }
6884
+ async init() {
6885
+ if (this.db)
6886
+ return;
6887
+ if (this.initPromise)
6888
+ return this.initPromise;
6889
+ this.initPromise = new Promise((resolve, reject) => {
6890
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
6891
+ request.onerror = () => reject(request.error);
6892
+ request.onsuccess = () => {
6893
+ this.db = request.result;
6894
+ resolve();
6895
+ };
6896
+ request.onupgradeneeded = (event) => {
6897
+ const db = event.target.result;
6898
+ // Create object stores if they don't exist
6899
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.PARTICIPANTS)) {
6900
+ db.createObjectStore(AXConversationIndexedDbStores.PARTICIPANTS, { keyPath: 'id' });
6901
+ }
6902
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.CONVERSATIONS)) {
6903
+ const convStore = db.createObjectStore(AXConversationIndexedDbStores.CONVERSATIONS, { keyPath: 'id' });
6904
+ convStore.createIndex('lastMessageAt', 'lastMessageAt', { unique: false });
6905
+ }
6906
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MESSAGES)) {
6907
+ const msgStore = db.createObjectStore(AXConversationIndexedDbStores.MESSAGES, { keyPath: 'id' });
6908
+ msgStore.createIndex('conversationId', 'conversationId', { unique: false });
6909
+ msgStore.createIndex('timestamp', 'timestamp', { unique: false });
6910
+ }
6911
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.SETTINGS)) {
6912
+ db.createObjectStore(AXConversationIndexedDbStores.SETTINGS, { keyPath: 'key' });
6913
+ }
6914
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MEDIA)) {
6915
+ db.createObjectStore(AXConversationIndexedDbStores.MEDIA, { keyPath: 'id' });
6916
+ }
6917
+ };
6918
+ });
6919
+ return this.initPromise;
6887
6920
  }
6888
6921
  /**
6889
- * Check if message should show avatar
6922
+ * Get a value from a store
6890
6923
  */
6891
- static shouldShowAvatar(message, previousMessage, conversation) {
6892
- // Always show avatar for group conversations
6893
- if (conversation.type === 'group' || conversation.type === 'channel') {
6894
- // Don't show if same sender as previous message within 5 minutes
6895
- if (previousMessage && previousMessage.senderId === message.senderId) {
6896
- const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
6897
- return timeDiff > 5 * 60 * 1000; // 5 minutes
6898
- }
6899
- return true;
6900
- }
6901
- return false;
6924
+ async get(storeName, key) {
6925
+ await this.init();
6926
+ if (!this.db)
6927
+ throw new Error('Database not initialized');
6928
+ const db = this.db;
6929
+ return new Promise((resolve, reject) => {
6930
+ const transaction = db.transaction(storeName, 'readonly');
6931
+ const store = transaction.objectStore(storeName);
6932
+ const request = store.get(key);
6933
+ request.onsuccess = () => resolve(request.result);
6934
+ request.onerror = () => reject(request.error);
6935
+ });
6902
6936
  }
6903
6937
  /**
6904
- * Group messages by sender for consecutive messages
6938
+ * Get all values from a store
6905
6939
  */
6906
- static shouldGroupWithPrevious(message, previousMessage) {
6907
- if (!previousMessage)
6908
- return false;
6909
- // Same sender
6910
- if (message.senderId !== previousMessage.senderId)
6911
- return false;
6912
- // Within 5 minutes
6913
- const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
6914
- return timeDiff < 5 * 60 * 1000;
6940
+ async getAll(storeName) {
6941
+ await this.init();
6942
+ if (!this.db)
6943
+ throw new Error('Database not initialized');
6944
+ const db = this.db;
6945
+ return new Promise((resolve, reject) => {
6946
+ const transaction = db.transaction(storeName, 'readonly');
6947
+ const store = transaction.objectStore(storeName);
6948
+ const request = store.getAll();
6949
+ request.onsuccess = () => resolve(request.result);
6950
+ request.onerror = () => reject(request.error);
6951
+ });
6915
6952
  }
6916
6953
  /**
6917
- * Get conversation status for avatar (private conversations only)
6954
+ * Get values by index
6918
6955
  */
6919
- static getConversationStatus(conversation) {
6920
- if (conversation.type === 'private') {
6921
- return conversation.status.presence;
6922
- }
6923
- return undefined;
6956
+ async getAllByIndex(storeName, indexName, query) {
6957
+ await this.init();
6958
+ if (!this.db)
6959
+ throw new Error('Database not initialized');
6960
+ const db = this.db;
6961
+ return new Promise((resolve, reject) => {
6962
+ const transaction = db.transaction(storeName, 'readonly');
6963
+ const store = transaction.objectStore(storeName);
6964
+ const index = store.index(indexName);
6965
+ const request = index.getAll(query);
6966
+ request.onsuccess = () => resolve(request.result);
6967
+ request.onerror = () => reject(request.error);
6968
+ });
6924
6969
  }
6925
6970
  /**
6926
- * Get typing indicator text for conversation
6971
+ * Put a value into a store
6927
6972
  */
6928
- static getTypingText(conversation) {
6929
- const typingUsers = conversation.status.typingUsers;
6930
- if (typingUsers.length === 0)
6931
- return '';
6932
- if (conversation.type === 'private') {
6933
- return translateSync('@acorex:chat.status.typing');
6934
- }
6935
- const firstUser = conversation.participants.find((p) => p.id === typingUsers[0]);
6936
- if (typingUsers.length === 1) {
6937
- return translateSync('@acorex:chat.status.user-is-typing', {
6938
- params: { userName: firstUser?.name || translateSync('@acorex:chat.fallbacks.someone') },
6939
- });
6940
- }
6941
- return translateSync('@acorex:chat.status.people-typing', { params: { count: typingUsers.length } });
6973
+ async put(storeName, value) {
6974
+ await this.init();
6975
+ if (!this.db)
6976
+ throw new Error('Database not initialized');
6977
+ const db = this.db;
6978
+ return new Promise((resolve, reject) => {
6979
+ const transaction = db.transaction(storeName, 'readwrite');
6980
+ const store = transaction.objectStore(storeName);
6981
+ const request = store.put(value);
6982
+ request.onsuccess = () => resolve();
6983
+ request.onerror = () => reject(request.error);
6984
+ });
6985
+ }
6986
+ /**
6987
+ * Delete a value from a store
6988
+ */
6989
+ async delete(storeName, key) {
6990
+ await this.init();
6991
+ if (!this.db)
6992
+ throw new Error('Database not initialized');
6993
+ const db = this.db;
6994
+ return new Promise((resolve, reject) => {
6995
+ const transaction = db.transaction(storeName, 'readwrite');
6996
+ const store = transaction.objectStore(storeName);
6997
+ const request = store.delete(key);
6998
+ request.onsuccess = () => resolve();
6999
+ request.onerror = () => reject(request.error);
7000
+ });
7001
+ }
7002
+ /**
7003
+ * Clear all data from a store
7004
+ */
7005
+ async clear(storeName) {
7006
+ await this.init();
7007
+ if (!this.db)
7008
+ throw new Error('Database not initialized');
7009
+ const db = this.db;
7010
+ return new Promise((resolve, reject) => {
7011
+ const transaction = db.transaction(storeName, 'readwrite');
7012
+ const store = transaction.objectStore(storeName);
7013
+ const request = store.clear();
7014
+ request.onsuccess = () => resolve();
7015
+ request.onerror = () => reject(request.error);
7016
+ });
7017
+ }
7018
+ // =====================
7019
+ // Convenience Methods
7020
+ // =====================
7021
+ async getParticipant(id) {
7022
+ return this.get(AXConversationIndexedDbStores.PARTICIPANTS, id);
7023
+ }
7024
+ async getAllParticipants() {
7025
+ return this.getAll(AXConversationIndexedDbStores.PARTICIPANTS);
7026
+ }
7027
+ async putParticipant(participant) {
7028
+ return this.put(AXConversationIndexedDbStores.PARTICIPANTS, participant);
7029
+ }
7030
+ async getConversation(id) {
7031
+ return this.get(AXConversationIndexedDbStores.CONVERSATIONS, id);
7032
+ }
7033
+ async getAllConversations() {
7034
+ return this.getAll(AXConversationIndexedDbStores.CONVERSATIONS);
7035
+ }
7036
+ async putConversation(conversation) {
7037
+ return this.put(AXConversationIndexedDbStores.CONVERSATIONS, conversation);
7038
+ }
7039
+ async deleteConversation(id) {
7040
+ return this.delete(AXConversationIndexedDbStores.CONVERSATIONS, id);
7041
+ }
7042
+ async getMessage(id) {
7043
+ return this.get(AXConversationIndexedDbStores.MESSAGES, id);
7044
+ }
7045
+ async getAllMessages() {
7046
+ return this.getAll(AXConversationIndexedDbStores.MESSAGES);
7047
+ }
7048
+ async getMessagesByConversation(conversationId) {
7049
+ return this.getAllByIndex(AXConversationIndexedDbStores.MESSAGES, 'conversationId', conversationId);
6942
7050
  }
6943
- /**
6944
- * Format last seen time
6945
- */
6946
- static formatLastSeen(date) {
6947
- const now = new Date();
6948
- const diff = now.getTime() - date.getTime();
6949
- const seconds = Math.floor(diff / 1000);
6950
- if (seconds < 60)
6951
- return translateSync('@acorex:chat.time.just-now');
6952
- if (seconds < 3600)
6953
- return translateSync('@acorex:chat.time.minutes-ago', { params: { count: Math.floor(seconds / 60) } });
6954
- if (seconds < 86400)
6955
- return translateSync('@acorex:chat.time.hours-ago', { params: { count: Math.floor(seconds / 3600) } });
6956
- if (seconds < 604800)
6957
- return translateSync('@acorex:chat.time.days-ago', { params: { count: Math.floor(seconds / 86400) } });
6958
- return date.toLocaleDateString();
7051
+ async putMessage(message) {
7052
+ return this.put(AXConversationIndexedDbStores.MESSAGES, message);
6959
7053
  }
6960
- /**
6961
- * Get conversation subtitle (status or member count)
6962
- */
6963
- static getConversationSubtitle(conversation) {
6964
- if (conversation.status.isTyping) {
6965
- return AXConversationMessageUtilsService.getTypingText(conversation);
6966
- }
6967
- switch (conversation.type) {
6968
- case 'private':
6969
- if (conversation.status.presence === 'online') {
6970
- return translateSync('@acorex:chat.status.online');
6971
- }
6972
- if (conversation.status.lastSeen) {
6973
- return translateSync('@acorex:chat.status.last-seen', {
6974
- params: { value: AXConversationMessageUtilsService.formatLastSeen(conversation.status.lastSeen) },
6975
- });
6976
- }
6977
- return translateSync('@acorex:chat.status.offline');
6978
- case 'group':
6979
- return translateSync('@acorex:chat.members.count', { params: { count: conversation.participants.length } });
6980
- case 'channel':
6981
- return translateSync('@acorex:chat.members.subscribers-count', { params: { count: conversation.participants.length } });
6982
- case 'bot':
6983
- return translateSync('@acorex:chat.bot');
6984
- default:
6985
- return '';
6986
- }
7054
+ async deleteMessage(id) {
7055
+ return this.delete(AXConversationIndexedDbStores.MESSAGES, id);
7056
+ }
7057
+ async getSetting(key) {
7058
+ const result = await this.get(AXConversationIndexedDbStores.SETTINGS, key);
7059
+ return result?.value;
7060
+ }
7061
+ async putSetting(key, value) {
7062
+ return this.put(AXConversationIndexedDbStores.SETTINGS, { key, value });
7063
+ }
7064
+ async getMedia(id) {
7065
+ return this.get(AXConversationIndexedDbStores.MEDIA, id);
7066
+ }
7067
+ async putMedia(record) {
7068
+ return this.put(AXConversationIndexedDbStores.MEDIA, record);
7069
+ }
7070
+ async deleteMedia(id) {
7071
+ return this.delete(AXConversationIndexedDbStores.MEDIA, id);
6987
7072
  }
6988
7073
  }
7074
+ // Export singleton instance
7075
+ const axConversationIndexedDbStorage = new AXConversationIndexedDbStorage();
7076
+
7077
+ var indexeddbStorage = /*#__PURE__*/Object.freeze({
7078
+ __proto__: null,
7079
+ AXConversationIndexedDbStorage: AXConversationIndexedDbStorage,
7080
+ AXConversationIndexedDbStores: AXConversationIndexedDbStores,
7081
+ axConversationIndexedDbStorage: axConversationIndexedDbStorage
7082
+ });
6989
7083
 
6990
7084
  /**
6991
7085
  * Shared read/write helpers for IndexedDB chat storage (in-memory + persistence).
@@ -8431,20 +8525,27 @@ class AXConversationIndexedDbConversationApi extends AXConversationApi {
8431
8525
  // Conversation CRUD
8432
8526
  // =====================
8433
8527
  async createConversation(data) {
8528
+ const creatorId = data.creatorId ?? conversationSharedStorage.currentUserId;
8529
+ const participantIds = new Set(data.participantIds);
8530
+ if (creatorId) {
8531
+ participantIds.add(creatorId);
8532
+ }
8434
8533
  const conversation = {
8435
8534
  id: `conv-${Date.now()}`,
8436
8535
  type: data.type,
8437
- title: data.title || (data.type === 'private' ? 'New Chat' : data.type === 'group' ? 'New Group' : 'New Channel'),
8536
+ creatorId,
8537
+ title: data.title ||
8538
+ (data.type === 'private' ? 'New Chat' : data.type === 'group' ? 'New Group' : 'New Channel'),
8438
8539
  avatar: data.avatar,
8439
8540
  description: data.description,
8440
- participants: data.participantIds.map((id) => {
8541
+ participants: [...participantIds].map((id) => {
8441
8542
  const participant = conversationSharedStorage.participants.get(id);
8442
8543
  if (!participant) {
8443
8544
  throw this.createError('PARTICIPANT_NOT_FOUND', `Participant ${id} not found`, 404);
8444
8545
  }
8445
8546
  return {
8446
8547
  ...participant,
8447
- role: id === conversationSharedStorage.currentUserId ? 'admin' : 'member',
8548
+ role: id === creatorId ? 'admin' : 'member',
8448
8549
  };
8449
8550
  }),
8450
8551
  lastMessageAt: new Date(),
@@ -10414,41 +10515,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
10414
10515
  `, styles: [":host{display:block}.avatar-picker{display:flex;flex-flow:row wrap;align-items:center;gap:1.5rem;padding:1rem;border-radius:.75rem;border:1px solid rgb(var(--ax-sys-color-border-light-surface))}.avatar-preview{position:relative;width:100px;height:100px;border-radius:50%;overflow:hidden;background:rgb(var(--ax-sys-color-lighter-surface));border:3px solid rgb(var(--ax-sys-color-border-light-surface));display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .25s ease;flex-shrink:0;box-shadow:0 2px 8px #00000014}.avatar-preview:hover{border-color:rgb(var(--ax-sys-color-primary-500));box-shadow:0 4px 12px #0000001f}.avatar-preview:hover .preview-overlay{opacity:1}.avatar-preview ax-image{width:100%;height:100%}.preview-overlay{position:absolute;top:0;right:0;bottom:0;left:0;background:#000000a6;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .25s ease;color:#fff;font-size:1.75rem}.avatar-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.375rem;font-size:2rem;color:rgb(var(--ax-sys-color-on-surface-variant));text-align:center;padding:.5rem}.placeholder-text{font-size:.625rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;opacity:.8}.avatar-actions{display:flex;flex-direction:column;gap:.5rem;flex:1}.avatar-actions ax-button{width:100%}\n"] }]
10415
10516
  }], propDecorators: { fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], showClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], avatarUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarUrl", required: false }] }, { type: i0.Output, args: ["avatarUrlChange"] }], onFileSelected: [{ type: i0.Output, args: ["onFileSelected"] }] } });
10416
10517
 
10417
- /** Other participant in a private chat (excludes the current user). */
10418
- function resolvePrivatePeerUserId(conversation, currentUserId) {
10419
- if (conversation.type !== 'private') {
10420
- return undefined;
10421
- }
10422
- const currentId = currentUserId ?? 'current-user';
10423
- return conversation.participants.find((participant) => participant.id !== currentId)?.id;
10424
- }
10425
- /** Whether `auto` kind should render a user avatar for this conversation. */
10426
- function shouldUseUserAvatarForConversation(conversation, currentUserId) {
10427
- return conversation.type === 'private' && !!resolvePrivatePeerUserId(conversation, currentUserId);
10428
- }
10429
- function resolveUserAvatarDisplay(userId, conversation, message) {
10430
- if (message && conversation) {
10431
- return {
10432
- name: AXConversationMessageUtilsService.getSenderName(message, conversation),
10433
- avatar: AXConversationMessageUtilsService.getSenderAvatar(message, conversation),
10434
- icon: AXConversationMessageUtilsService.getSenderAvatarIcon(message, conversation),
10435
- };
10436
- }
10437
- const participant = conversation?.participants.find((p) => p.id === userId);
10438
- return {
10439
- name: participant?.name ?? userId,
10440
- avatar: participant?.avatar,
10441
- icon: participant?.icon,
10442
- };
10443
- }
10444
- function resolveConversationAvatarDisplay(conversation) {
10445
- return {
10446
- name: conversation.title,
10447
- avatar: AXConversationMessageUtilsService.getConversationAvatar(conversation),
10448
- icon: AXConversationMessageUtilsService.getConversationAvatarIcon(conversation),
10449
- };
10450
- }
10451
-
10452
10518
  /**
10453
10519
  * Conversation avatar host — renders a registered custom component or built-in fallback.
10454
10520
  */
@@ -10533,7 +10599,7 @@ class AXConversationAvatarComponent {
10533
10599
  if (!conversation) {
10534
10600
  return { name: explicitName ?? '?', avatar: explicitAvatar, icon: explicitIcon };
10535
10601
  }
10536
- const resolved = resolveConversationAvatarDisplay(conversation);
10602
+ const resolved = resolveConversationAvatarDisplay(conversation, this.currentUserId());
10537
10603
  return {
10538
10604
  name: explicitName ?? resolved.name,
10539
10605
  avatar: explicitAvatar ?? resolved.avatar,
@@ -10684,6 +10750,58 @@ class AXConversationDateUtilsService {
10684
10750
  }
10685
10751
  }
10686
10752
 
10753
+ function toUploaderReference(item) {
10754
+ return {
10755
+ url: item.url,
10756
+ mediaId: item.mediaId,
10757
+ mimeType: item.mimeType,
10758
+ size: item.size,
10759
+ };
10760
+ }
10761
+
10762
+ /**
10763
+ * Resolves multimedia payload URLs for renderers (direct url or mediaId via uploader).
10764
+ */
10765
+ /**
10766
+ * Creates a signal of the resolved playback URL for a media reference.
10767
+ */
10768
+ function createResolvedMediaUrlSignal(reference) {
10769
+ const uploader = inject(AXUploaderService);
10770
+ const destroyRef = inject(DestroyRef);
10771
+ const resolvedUrl = signal(null, ...(ngDevMode ? [{ debugName: "resolvedUrl" }] : /* istanbul ignore next */ []));
10772
+ let abortController = null;
10773
+ effect(() => {
10774
+ const ref = reference();
10775
+ abortController?.abort();
10776
+ abortController = new AbortController();
10777
+ const trimmedUrl = ref?.url?.trim();
10778
+ if (!trimmedUrl && !ref?.mediaId) {
10779
+ resolvedUrl.set(null);
10780
+ return;
10781
+ }
10782
+ const directUrl = trimmedUrl && !isNonPersistableMediaUrl(trimmedUrl) ? trimmedUrl : undefined;
10783
+ if (!ref?.mediaId) {
10784
+ resolvedUrl.set(directUrl ?? null);
10785
+ return;
10786
+ }
10787
+ resolvedUrl.set(directUrl ?? null);
10788
+ void uploader
10789
+ .resolvePlaybackUrl(ref, abortController.signal)
10790
+ .then((url) => {
10791
+ if (!abortController?.signal.aborted) {
10792
+ resolvedUrl.set(url);
10793
+ }
10794
+ })
10795
+ .catch(() => {
10796
+ if (!abortController?.signal.aborted && directUrl) {
10797
+ resolvedUrl.set(directUrl);
10798
+ }
10799
+ });
10800
+ });
10801
+ destroyRef.onDestroy(() => abortController?.abort());
10802
+ return resolvedUrl.asReadonly();
10803
+ }
10804
+
10687
10805
  function isMessageDeliveryPending(status) {
10688
10806
  return status === 'sending';
10689
10807
  }
@@ -10882,15 +11000,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
10882
11000
  `, styles: [":host{display:block}.ax-cnv-fallback{display:flex;flex-direction:column;gap:.375rem;max-width:min(100%,18rem)}.ax-cnv-renderer-state__row{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;padding:.375rem .5rem;border-radius:var(--ax-sys-border-radius);font-size:.8125rem;line-height:1.35}.ax-cnv-renderer-state__row--warn{background:rgb(var(--ax-sys-color-warning-lightest-surface));color:rgb(var(--ax-sys-color-warning-800))}.ax-cnv-renderer-state__msg{flex:1;min-width:0}\n"] }]
10883
11001
  }], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }] } });
10884
11002
 
10885
- function toUploaderReference(item) {
10886
- return {
10887
- url: item.url,
10888
- mediaId: item.mediaId,
10889
- mimeType: item.mimeType,
10890
- size: item.size,
10891
- };
10892
- }
10893
-
10894
11003
  /** Shared formatting for message renderer UIs. */
10895
11004
  function formatMediaDuration(seconds) {
10896
11005
  if (!Number.isFinite(seconds)) {
@@ -10910,49 +11019,6 @@ function formatFileByteSize(bytes) {
10910
11019
  return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
10911
11020
  }
10912
11021
 
10913
- /**
10914
- * Resolves multimedia payload URLs for renderers (direct url or mediaId via uploader).
10915
- */
10916
- /**
10917
- * Creates a signal of the resolved playback URL for a media reference.
10918
- */
10919
- function createResolvedMediaUrlSignal(reference) {
10920
- const uploader = inject(AXUploaderService);
10921
- const destroyRef = inject(DestroyRef);
10922
- const resolvedUrl = signal(null, ...(ngDevMode ? [{ debugName: "resolvedUrl" }] : /* istanbul ignore next */ []));
10923
- let abortController = null;
10924
- effect(() => {
10925
- const ref = reference();
10926
- abortController?.abort();
10927
- abortController = new AbortController();
10928
- const trimmedUrl = ref?.url?.trim();
10929
- if (!trimmedUrl && !ref?.mediaId) {
10930
- resolvedUrl.set(null);
10931
- return;
10932
- }
10933
- const directUrl = trimmedUrl && !isNonPersistableMediaUrl(trimmedUrl) ? trimmedUrl : undefined;
10934
- if (!ref?.mediaId) {
10935
- resolvedUrl.set(directUrl ?? null);
10936
- return;
10937
- }
10938
- resolvedUrl.set(directUrl ?? null);
10939
- void uploader
10940
- .resolvePlaybackUrl(ref, abortController.signal)
10941
- .then((url) => {
10942
- if (!abortController?.signal.aborted) {
10943
- resolvedUrl.set(url);
10944
- }
10945
- })
10946
- .catch(() => {
10947
- if (!abortController?.signal.aborted && directUrl) {
10948
- resolvedUrl.set(directUrl);
10949
- }
10950
- });
10951
- });
10952
- destroyRef.onDestroy(() => abortController?.abort());
10953
- return resolvedUrl.asReadonly();
10954
- }
10955
-
10956
11022
  class AXAudioAttachmentComponent {
10957
11023
  constructor() {
10958
11024
  this.item = input.required(...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
@@ -13027,6 +13093,18 @@ async function copyWithFileTypeFallback(message, registry, fallback) {
13027
13093
  const result = await fileType.copy(message.payload);
13028
13094
  return resolveFileCopyText(result)?.trim() || fallback();
13029
13095
  }
13096
+ /** Use file-type `open` on the message when `message.fileType` is set. */
13097
+ async function openWithFileType(message, registry, ctx) {
13098
+ if (!message.fileType) {
13099
+ return false;
13100
+ }
13101
+ const fileType = await registry.get(message.fileType);
13102
+ if (!fileType?.open) {
13103
+ return false;
13104
+ }
13105
+ const result = await runFileTypeOpen(fileType, ctx, message.payload);
13106
+ return result !== false;
13107
+ }
13030
13108
  /**
13031
13109
  * Registers the live renderer on {@link AXMessageListService} for message actions.
13032
13110
  */
@@ -14937,14 +15015,23 @@ class AXConversationService {
14937
15015
  this._messageDeleted$ = new Subject();
14938
15016
  this._typingIndicator$ = new Subject();
14939
15017
  this._presenceUpdate$ = new Subject();
14940
- /** All conversations */
14941
- this.conversations = this.state.conversations;
15018
+ /** All conversations (viewer-scoped display titles for private 1v1 chats) */
15019
+ this.conversations = computed(() => {
15020
+ const currentUserId = this._currentUser()?.id;
15021
+ return this.state.conversations().map((conversation) => resolveConversationForViewer(conversation, currentUserId));
15022
+ }, ...(ngDevMode ? [{ debugName: "conversations" }] : /* istanbul ignore next */ []));
14942
15023
  /** Active conversation ID */
14943
15024
  this.activeConversationId = this._activeConversationId.asReadonly();
14944
15025
  /** Active conversation */
14945
15026
  this.activeConversation = computed(() => {
14946
15027
  const id = this._activeConversationId();
14947
- return id ? this.state.getConversation(id) : null;
15028
+ if (!id) {
15029
+ return null;
15030
+ }
15031
+ const conversation = this.state.getConversation(id);
15032
+ return conversation
15033
+ ? resolveConversationForViewer(conversation, this._currentUser()?.id)
15034
+ : null;
14948
15035
  }, ...(ngDevMode ? [{ debugName: "activeConversation" }] : /* istanbul ignore next */ []));
14949
15036
  /** Messages for active conversation */
14950
15037
  this.activeMessages = computed(() => {
@@ -15735,17 +15822,29 @@ class AXConversationService {
15735
15822
  */
15736
15823
  async createConversation(participantIds, type, metadata) {
15737
15824
  try {
15825
+ const currentUser = this._currentUser();
15826
+ const creatorId = currentUser?.id;
15827
+ const isPrivate = type === 'private';
15828
+ const scopedParticipantIds = [...participantIds];
15829
+ if (creatorId && !scopedParticipantIds.includes(creatorId)) {
15830
+ scopedParticipantIds.unshift(creatorId);
15831
+ }
15738
15832
  const conversation = await this.conversationApi.createConversation({
15739
15833
  type,
15740
- participantIds,
15741
- title: metadata?.['title'],
15834
+ participantIds: scopedParticipantIds,
15835
+ creatorId,
15836
+ title: isPrivate ? undefined : metadata?.['title'],
15742
15837
  description: metadata?.['description'],
15743
- avatar: AXConversationService.normalizeOptionalString(metadata?.['avatar']),
15744
- icon: AXConversationService.normalizeOptionalString(metadata?.['icon']),
15745
- metadata,
15838
+ avatar: isPrivate
15839
+ ? undefined
15840
+ : AXConversationService.normalizeOptionalString(metadata?.['avatar']),
15841
+ icon: isPrivate
15842
+ ? undefined
15843
+ : AXConversationService.normalizeOptionalString(metadata?.['icon']),
15844
+ metadata: isPrivate ? undefined : metadata,
15746
15845
  });
15747
15846
  this.state.setConversation(conversation);
15748
- return conversation;
15847
+ return resolveConversationForViewer(conversation, creatorId);
15749
15848
  }
15750
15849
  catch (error) {
15751
15850
  this.errorHandler.handle(error, 'createConversation', { participantIds, type });
@@ -16453,8 +16552,6 @@ class AXComposerComponent {
16453
16552
  this.composerPopupComponent = AXComposerPopupComponent;
16454
16553
  /** Input placeholder - use service */
16455
16554
  this.placeholder = this.composerService.placeholder;
16456
- /** Sending message state */
16457
- this.sendingMessage = signal(false, ...(ngDevMode ? [{ debugName: "sendingMessage" }] : /* istanbul ignore next */ []));
16458
16555
  /** Typing indicator subject for rate limiting */
16459
16556
  this.typingSubject = new Subject();
16460
16557
  // Focus text area when requested via AXComposerService.focusComposer()
@@ -16581,51 +16678,44 @@ class AXComposerComponent {
16581
16678
  }
16582
16679
  /** Handle send click */
16583
16680
  async onSendClick() {
16584
- if (!this.canSend() || this.sendingMessage())
16681
+ if (!this.canSend())
16585
16682
  return;
16586
16683
  const conversation = this.activeConversation();
16587
16684
  if (!conversation)
16588
16685
  return;
16589
16686
  const text = this.draftText().trim();
16590
16687
  const editingMsg = this.editingMessage();
16591
- this.sendingMessage.set(true);
16592
- try {
16593
- // Check if we're editing a message
16594
- if (editingMsg) {
16595
- // Edit existing message
16688
+ if (editingMsg) {
16689
+ try {
16596
16690
  await this.conversationService.editMessage(editingMsg.id, { type: 'text', text });
16597
- }
16598
- else {
16599
- // Create send command for new message
16600
- const command = {
16601
- conversationId: conversation.id,
16602
- type: 'text',
16603
- payload: { type: 'text', text },
16604
- replyToId: this.replyingToMessage()?.id,
16605
- };
16606
- // Send new message
16607
- await this.composerService.sendMessage(command);
16608
- this.messageSent.emit(command);
16609
- // Request message list to scroll to bottom after sending
16610
- this.messageListService.requestScrollToBottom();
16611
- }
16612
- // Clear input and draft
16613
- this.draftText.set('');
16614
- this.attachments.set([]);
16615
- this.cancelEditReply();
16616
- this.composerService.clear();
16617
- // Clear saved draft after successful send
16618
- if (conversation.id) {
16691
+ this.draftText.set('');
16692
+ this.attachments.set([]);
16693
+ this.cancelEditReply();
16619
16694
  this.composerService.clearDraft(conversation.id);
16695
+ this.composerService.focusComposer();
16620
16696
  }
16621
- this.composerService.focusComposer();
16622
- }
16623
- catch (error) {
16624
- console.error('Failed to send/edit message:', error);
16625
- }
16626
- finally {
16627
- this.sendingMessage.set(false);
16697
+ catch (error) {
16698
+ console.error('Failed to edit message:', error);
16699
+ }
16700
+ return;
16628
16701
  }
16702
+ const command = {
16703
+ conversationId: conversation.id,
16704
+ type: 'text',
16705
+ payload: { type: 'text', text },
16706
+ replyToId: this.replyingToMessage()?.id,
16707
+ };
16708
+ this.editingMessage.set(null);
16709
+ this.replyingToMessage.set(null);
16710
+ void this.composerService
16711
+ .sendMessage(command)
16712
+ .then(() => {
16713
+ this.messageSent.emit(command);
16714
+ this.messageListService.requestScrollToBottom();
16715
+ })
16716
+ .catch((error) => {
16717
+ console.error('Failed to send message:', error);
16718
+ });
16629
16719
  }
16630
16720
  /** Handle emoji click */
16631
16721
  async onEmojiClick(event) {
@@ -16679,16 +16769,14 @@ class AXComposerComponent {
16679
16769
  },
16680
16770
  replyToId: this.replyingToMessage()?.id,
16681
16771
  };
16682
- try {
16683
- await this.composerService.sendMessage(command);
16684
- // Clear reply mode after sending
16685
- this.cancelEditReply();
16686
- // Request message list to scroll to bottom after sending sticker
16772
+ void this.composerService
16773
+ .sendMessage(command)
16774
+ .then(() => {
16687
16775
  this.messageListService.requestScrollToBottom();
16688
- }
16689
- catch (error) {
16776
+ })
16777
+ .catch((error) => {
16690
16778
  console.error('Failed to send sticker:', error);
16691
- }
16779
+ });
16692
16780
  }
16693
16781
  /** Get inputs for emoji popup component */
16694
16782
  getEmojiPopupInputs() {
@@ -20976,12 +21064,7 @@ class AXNewConversationDialogComponent extends AXBasePageComponent {
20976
21064
  try {
20977
21065
  let conversation;
20978
21066
  if (selectedIds.length === 1) {
20979
- const selectedUser = this.availableUsers().find((u) => u.id === selectedIds[0]);
20980
- const metadata = {
20981
- title: selectedUser?.name,
20982
- avatar: selectedUser?.avatar,
20983
- };
20984
- conversation = await this.conversationService.createConversation(selectedIds, 'private', metadata);
21067
+ conversation = await this.conversationService.createConversation(selectedIds, 'private');
20985
21068
  }
20986
21069
  else {
20987
21070
  const metadata = {
@@ -22195,5 +22278,5 @@ function getErrorMessage(code, params) {
22195
22278
  * Generated bundle index. Do not edit.
22196
22279
  */
22197
22280
 
22198
- export { AXAudioPickerComponent, AXAudioRendererComponent, AXBaseRegistry, AXCnvMediaPlaybackInfoBarBannerComponent, AXComposerActionRegistry, AXComposerComponent, AXComposerPopupComponent, AXComposerService, AXComposerTabRegistry, AXConversation2Module, AXConversationAiApiKey, AXConversationAiResponderService, AXConversationApi, AXConversationContainerComponent, AXConversationContainerDirective, AXConversationDateUtilsService, AXConversationIndexedDbConversationApi, AXConversationIndexedDbMessageAiApi, AXConversationIndexedDbMessageApi, AXConversationIndexedDbRealtimeApi, AXConversationIndexedDbStorage, AXConversationIndexedDbStores, AXConversationIndexedDbUserApi, AXConversationInfoMediaViewComponent, AXConversationInfoPanelComponent, AXConversationItemActionRegistry, AXConversationMessageRendererStateComponent, AXConversationMessageUtilsService, AXConversationService, AXConversationSharedStorage, AXConversationTabRegistry, AXEmojiTabComponent, AXErrorHandlerService, AXFallbackRendererComponent, AXFilePickerComponent, AXFileRendererComponent, AXForwardMessageDialogComponent, AXImagePickerComponent, AXImageRendererComponent, AXInfiniteScrollDirective, AXInfoBarActionRegistry, AXInfoBarComponent, AXInfoBarSearchComponent, AXInfoBarService, AXLocationPickerComponent, AXLocationRendererComponent, AXMessageActionRegistry, AXMessageApi, AXMessageListComponent, AXMessageListNoActiveDefaultComponent, AXMessageListService, AXMessageRendererRegistry, AXNewConversationDialogComponent, AXPickerFooterComponent, AXPickerHeaderComponent, AXRealtimeApi, AXRegistryService, AXSidebarComponent, AXSidebarService, AXStickerRendererComponent, AXStickerTabComponent, AXSystemRendererComponent, AXTextRendererComponent, AXUserApi, AXVideoPickerComponent, AXVideoRendererComponent, AXVoiceRecorderComponent, AXVoiceRendererComponent, AX_CONVERSATION_AUDIO_RENDERER, AX_CONVERSATION_COMPOSER_AUDIO_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_TAB, AX_CONVERSATION_COMPOSER_FILE_ACTION, AX_CONVERSATION_COMPOSER_FILE_TYPE_PROVIDERS, AX_CONVERSATION_COMPOSER_IMAGE_ACTION, AX_CONVERSATION_COMPOSER_LOCATION_ACTION, AX_CONVERSATION_COMPOSER_STICKER_TAB, AX_CONVERSATION_COMPOSER_VIDEO_ACTION, AX_CONVERSATION_COMPOSER_VOICE_RECORDING_ACTION, AX_CONVERSATION_CONVERSATION_AVATAR_COMPONENT, AX_CONVERSATION_CURSOR_PREFIX, AX_CONVERSATION_FALLBACK_RENDERER, AX_CONVERSATION_FILE_RENDERER, AX_CONVERSATION_IMAGE_RENDERER, AX_CONVERSATION_INFO_BAR_ARCHIVE_ACTION, AX_CONVERSATION_INFO_BAR_BLOCK_ACTION, AX_CONVERSATION_INFO_BAR_DELETE_ACTION, AX_CONVERSATION_INFO_BAR_DIVIDER, AX_CONVERSATION_INFO_BAR_MUTE_ACTION, AX_CONVERSATION_INFO_BAR_SEARCH_ACTION, AX_CONVERSATION_ITEM_BLOCK_ACTION, AX_CONVERSATION_ITEM_DELETE_ACTION, AX_CONVERSATION_ITEM_DIVIDER, AX_CONVERSATION_ITEM_MARK_READ_ACTION, AX_CONVERSATION_ITEM_MUTE_ACTION, AX_CONVERSATION_ITEM_PIN_ACTION, AX_CONVERSATION_LOCATION_RENDERER, AX_CONVERSATION_MESSAGE_DELETE_ACTION, AX_CONVERSATION_MESSAGE_EDIT_ACTION, AX_CONVERSATION_MESSAGE_FORWARD_ACTION, AX_CONVERSATION_MESSAGE_REPLY_ACTION, AX_CONVERSATION_STICKER_RENDERER, AX_CONVERSATION_SYSTEM_RENDERER, AX_CONVERSATION_TAB_ALL, AX_CONVERSATION_TAB_ARCHIVED, AX_CONVERSATION_TAB_BOT, AX_CONVERSATION_TAB_CHANNELS, AX_CONVERSATION_TAB_GROUPS, AX_CONVERSATION_TAB_PRIVATE, AX_CONVERSATION_TAB_UNREAD, AX_CONVERSATION_TEXT_RENDERER, AX_CONVERSATION_USER_AVATAR_COMPONENT, AX_CONVERSATION_VIDEO_RENDERER, AX_CONVERSATION_VOICE_RENDERER, AX_DEFAULT_CONVERSATION_CONFIG, AX_MESSAGE_CURSOR_PREFIX, AX_STICKER_API_KEY, CONNECTION_ERRORS, CONVERSATION_CONFIG, CONVERSATION_ERRORS, CONVERSATION_FILE_TYPES_READY, DEFAULT_COMPOSER_ACTIONS, DEFAULT_COMPOSER_TABS, DEFAULT_CONVERSATION_ITEM_ACTIONS, DEFAULT_CONVERSATION_TABS, DEFAULT_INFO_BAR_ACTIONS, DEFAULT_MESSAGE_ACTIONS, DEFAULT_MESSAGE_RENDERERS, ERROR_HANDLER_CONFIG, ERROR_MESSAGES, FILE_ERRORS, LOCATION_ERRORS, MESSAGE_ERRORS, REGISTRY_CONFIG, URL_ERRORS, USER_ERRORS, applyConversationFilters, applyLocalPreview, axConversationIndexedDbStorage, conversationSharedStorage, createLocalPreviewUrl, createObjectUrl, encodeConversationCursor, encodeMessageCursor, formatErrorMessage, getConversationLastActivity, getConversationMessagesNewestFirst, getErrorMessage, getSortedConversationsForInbox, mergeUploadResult, mergeWithDefaults, normalizeAllConversationMessageIndexes, paginateChatNewestFirst, paginateChatOldestFirst, parseChatCursor, provideConversation, provideConversationComposerFileTypes, provideConversationFileCatalog, registerChatMessage, resolveConversationAvatarDisplay, resolveParticipantProfile, resolvePrivatePeerUserId, resolveUserAvatarDisplay, revokeObjectUrl, sanitizeInput, shouldUseUserAvatarForConversation, sortConversationMessageIds, toUploaderReference$1 as toUploaderReference, unregisterChatMessage, validateConversationId, validateEmail, validateLatitude, validateLongitude, validateMessagePayload, validateMessageText, validateMessageType, validateUrl, validateUserId, validateUserIds };
22281
+ export { AXAudioPickerComponent, AXAudioRendererComponent, AXBaseRegistry, AXCnvMediaPlaybackInfoBarBannerComponent, AXComposerActionRegistry, AXComposerComponent, AXComposerPopupComponent, AXComposerService, AXComposerTabRegistry, AXConversation2Module, AXConversationAiApiKey, AXConversationAiResponderService, AXConversationApi, AXConversationContainerComponent, AXConversationContainerDirective, AXConversationDateUtilsService, AXConversationIndexedDbConversationApi, AXConversationIndexedDbMessageAiApi, AXConversationIndexedDbMessageApi, AXConversationIndexedDbRealtimeApi, AXConversationIndexedDbStorage, AXConversationIndexedDbStores, AXConversationIndexedDbUserApi, AXConversationInfoMediaViewComponent, AXConversationInfoPanelComponent, AXConversationItemActionRegistry, AXConversationMessageRendererStateComponent, AXConversationMessageUtilsService, AXConversationService, AXConversationSharedStorage, AXConversationTabRegistry, AXEmojiTabComponent, AXErrorHandlerService, AXFallbackRendererComponent, AXFilePickerComponent, AXFileRendererComponent, AXForwardMessageDialogComponent, AXImagePickerComponent, AXImageRendererComponent, AXInfiniteScrollDirective, AXInfoBarActionRegistry, AXInfoBarComponent, AXInfoBarSearchComponent, AXInfoBarService, AXLocationPickerComponent, AXLocationRendererComponent, AXMessageActionRegistry, AXMessageApi, AXMessageListComponent, AXMessageListNoActiveDefaultComponent, AXMessageListService, AXMessageRendererCopyHostComponent, AXMessageRendererRegistry, AXNewConversationDialogComponent, AXPickerFooterComponent, AXPickerHeaderComponent, AXRealtimeApi, AXRegistryService, AXSidebarComponent, AXSidebarService, AXStickerRendererComponent, AXStickerTabComponent, AXSystemRendererComponent, AXTextRendererComponent, AXUserApi, AXVideoPickerComponent, AXVideoRendererComponent, AXVoiceRecorderComponent, AXVoiceRendererComponent, AX_CONVERSATION_AUDIO_RENDERER, AX_CONVERSATION_COMPOSER_AUDIO_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_TAB, AX_CONVERSATION_COMPOSER_FILE_ACTION, AX_CONVERSATION_COMPOSER_FILE_TYPE_PROVIDERS, AX_CONVERSATION_COMPOSER_IMAGE_ACTION, AX_CONVERSATION_COMPOSER_LOCATION_ACTION, AX_CONVERSATION_COMPOSER_STICKER_TAB, AX_CONVERSATION_COMPOSER_VIDEO_ACTION, AX_CONVERSATION_COMPOSER_VOICE_RECORDING_ACTION, AX_CONVERSATION_CONVERSATION_AVATAR_COMPONENT, AX_CONVERSATION_CURSOR_PREFIX, AX_CONVERSATION_FALLBACK_RENDERER, AX_CONVERSATION_FILE_RENDERER, AX_CONVERSATION_IMAGE_RENDERER, AX_CONVERSATION_INFO_BAR_ARCHIVE_ACTION, AX_CONVERSATION_INFO_BAR_BLOCK_ACTION, AX_CONVERSATION_INFO_BAR_DELETE_ACTION, AX_CONVERSATION_INFO_BAR_DIVIDER, AX_CONVERSATION_INFO_BAR_MUTE_ACTION, AX_CONVERSATION_INFO_BAR_SEARCH_ACTION, AX_CONVERSATION_ITEM_BLOCK_ACTION, AX_CONVERSATION_ITEM_DELETE_ACTION, AX_CONVERSATION_ITEM_DIVIDER, AX_CONVERSATION_ITEM_MARK_READ_ACTION, AX_CONVERSATION_ITEM_MUTE_ACTION, AX_CONVERSATION_ITEM_PIN_ACTION, AX_CONVERSATION_LOCATION_RENDERER, AX_CONVERSATION_MESSAGE_DELETE_ACTION, AX_CONVERSATION_MESSAGE_EDIT_ACTION, AX_CONVERSATION_MESSAGE_FORWARD_ACTION, AX_CONVERSATION_MESSAGE_REPLY_ACTION, AX_CONVERSATION_STICKER_RENDERER, AX_CONVERSATION_SYSTEM_RENDERER, AX_CONVERSATION_TAB_ALL, AX_CONVERSATION_TAB_ARCHIVED, AX_CONVERSATION_TAB_BOT, AX_CONVERSATION_TAB_CHANNELS, AX_CONVERSATION_TAB_GROUPS, AX_CONVERSATION_TAB_PRIVATE, AX_CONVERSATION_TAB_UNREAD, AX_CONVERSATION_TEXT_RENDERER, AX_CONVERSATION_USER_AVATAR_COMPONENT, AX_CONVERSATION_VIDEO_RENDERER, AX_CONVERSATION_VOICE_RENDERER, AX_DEFAULT_CONVERSATION_CONFIG, AX_MESSAGE_CURSOR_PREFIX, AX_STICKER_API_KEY, CONNECTION_ERRORS, CONVERSATION_CONFIG, CONVERSATION_ERRORS, CONVERSATION_FILE_TYPES_READY, DEFAULT_COMPOSER_ACTIONS, DEFAULT_COMPOSER_TABS, DEFAULT_CONVERSATION_ITEM_ACTIONS, DEFAULT_CONVERSATION_TABS, DEFAULT_INFO_BAR_ACTIONS, DEFAULT_MESSAGE_ACTIONS, DEFAULT_MESSAGE_RENDERERS, ERROR_HANDLER_CONFIG, ERROR_MESSAGES, FILE_ERRORS, LOCATION_ERRORS, MESSAGE_ERRORS, REGISTRY_CONFIG, URL_ERRORS, USER_ERRORS, applyConversationFilters, applyLocalPreview, axConversationIndexedDbStorage, conversationSharedStorage, copyWithFileTypeFallback, createLocalPreviewUrl, createObjectUrl, createResolvedMediaUrlSignal, encodeConversationCursor, encodeMessageCursor, formatErrorMessage, getConversationLastActivity, getConversationMessagesNewestFirst, getErrorMessage, getSortedConversationsForInbox, isGenericPrivateConversationTitle, mediaCopyText, mergeUploadResult, mergeWithDefaults, normalizeAllConversationMessageIndexes, normalizeImagePayload, openWithFileType, paginateChatNewestFirst, paginateChatOldestFirst, parseChatCursor, pickDisplayMediaUrl, provideConversation, provideConversationComposerFileTypes, provideConversationFileCatalog, registerChatMessage, resolveConversationAvatarDisplay, resolveConversationForViewer, resolveConversationTitleForViewer, resolveImageDisplayUrl, resolveParticipantProfile, resolvePrivatePeerParticipant, resolvePrivatePeerUserId, resolveUserAvatarDisplay, revokeObjectUrl, sanitizeInput, shouldUseUserAvatarForConversation, sortConversationMessageIds, toUploaderReference as toMediaItemUploaderReference, toUploaderReference$1 as toUploaderReference, unregisterChatMessage, validateConversationId, validateEmail, validateLatitude, validateLongitude, validateMessagePayload, validateMessageText, validateMessageType, validateUrl, validateUserId, validateUserIds };
22199
22282
  //# sourceMappingURL=acorex-components-conversation2.mjs.map