@acorex/components 20.8.20 → 20.8.22

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`.
@@ -2554,9 +2958,22 @@ class AXComposerService {
2554
2958
  await this.conversationService.sendTypingIndicator(conversationId);
2555
2959
  }
2556
2960
  async sendMessage(command, options) {
2961
+ this.clearComposerInput(command.conversationId);
2557
2962
  await this.conversationService.sendMessage(command, options);
2963
+ }
2964
+ /**
2965
+ * Resets composer input immediately when a send starts so upload/API work continues in the background.
2966
+ */
2967
+ clearComposerInput(conversationId) {
2968
+ this.draftText.set('');
2969
+ this.attachments.set([]);
2558
2970
  this.hideComponent();
2559
- this.clear();
2971
+ this._replyMessage.set(null);
2972
+ this._editMessage.set(null);
2973
+ if (conversationId) {
2974
+ this.clearDraft(conversationId);
2975
+ }
2976
+ this.focusComposer();
2560
2977
  }
2561
2978
  /**
2562
2979
  * Show a component in the composer (full-width mode)
@@ -3560,17 +3977,12 @@ class AXAudioPickerComponent {
3560
3977
  },
3561
3978
  };
3562
3979
  this.retainUploadedOnDismiss = true;
3563
- try {
3564
- await this.composerService.sendMessage(command);
3565
- const popup = this.__popup__();
3566
- if (popup) {
3567
- this.dismissPicker();
3568
- dismissComposerPickerHost(this.composerService, popup);
3569
- }
3570
- }
3571
- catch {
3980
+ const popup = this.__popup__();
3981
+ this.dismissPicker();
3982
+ dismissComposerPickerHost(this.composerService, popup);
3983
+ void this.composerService.sendMessage(command).catch(() => {
3572
3984
  this.retainUploadedOnDismiss = false;
3573
- }
3985
+ });
3574
3986
  }
3575
3987
  canSend() {
3576
3988
  const list = this.audioFiles();
@@ -3978,18 +4390,13 @@ class AXFilePickerComponent {
3978
4390
  caption: this.caption.trim() || undefined,
3979
4391
  },
3980
4392
  };
3981
- this.retainUploadedOnDismiss = true;
3982
- try {
3983
- await this.composerService.sendMessage(command);
3984
- const popup = this.__popup__();
3985
- if (popup) {
3986
- this.dismissPicker();
3987
- dismissComposerPickerHost(this.composerService, popup);
3988
- }
3989
- }
3990
- catch {
4393
+ this.retainUploadedOnDismiss = true;
4394
+ const popup = this.__popup__();
4395
+ this.dismissPicker();
4396
+ dismissComposerPickerHost(this.composerService, popup);
4397
+ void this.composerService.sendMessage(command).catch(() => {
3991
4398
  this.retainUploadedOnDismiss = false;
3992
- }
4399
+ });
3993
4400
  }
3994
4401
  updateFileProgress(index, progress, uploading, uploaded = false, uploadedUrl, mediaId, error) {
3995
4402
  if (this.uploadsDismissed) {
@@ -4441,17 +4848,12 @@ class AXImagePickerComponent {
4441
4848
  },
4442
4849
  };
4443
4850
  this.retainUploadedOnDismiss = true;
4444
- try {
4445
- await this.composerService.sendMessage(command);
4446
- const popup = this.__popup__();
4447
- if (popup) {
4448
- this.dismissPicker();
4449
- dismissComposerPickerHost(this.composerService, popup);
4450
- }
4451
- }
4452
- catch {
4851
+ const popup = this.__popup__();
4852
+ this.dismissPicker();
4853
+ dismissComposerPickerHost(this.composerService, popup);
4854
+ void this.composerService.sendMessage(command).catch(() => {
4453
4855
  this.retainUploadedOnDismiss = false;
4454
- }
4856
+ });
4455
4857
  }
4456
4858
  canSend() {
4457
4859
  const list = this.images();
@@ -4707,8 +5109,10 @@ class AXLocationPickerComponent {
4707
5109
  type: 'location',
4708
5110
  payload: location,
4709
5111
  };
4710
- await this.composerService.sendMessage(command);
4711
5112
  this.cancel();
5113
+ void this.composerService.sendMessage(command).catch((error) => {
5114
+ console.error('Failed to send location:', error);
5115
+ });
4712
5116
  }
4713
5117
  cancel() {
4714
5118
  // Reset to default location
@@ -4957,17 +5361,12 @@ class AXVideoPickerComponent {
4957
5361
  },
4958
5362
  };
4959
5363
  this.retainUploadedOnDismiss = true;
4960
- try {
4961
- await this.composerService.sendMessage(command);
4962
- const popup = this.__popup__();
4963
- if (popup) {
4964
- this.dismissPicker();
4965
- dismissComposerPickerHost(this.composerService, popup);
4966
- }
4967
- }
4968
- catch {
5364
+ const popup = this.__popup__();
5365
+ this.dismissPicker();
5366
+ dismissComposerPickerHost(this.composerService, popup);
5367
+ void this.composerService.sendMessage(command).catch(() => {
4969
5368
  this.retainUploadedOnDismiss = false;
4970
- }
5369
+ });
4971
5370
  }
4972
5371
  canSend() {
4973
5372
  const list = this.videos();
@@ -5374,44 +5773,44 @@ class AXVoiceRecorderComponent {
5374
5773
  const uploadSignal = this.uploadAbortController.signal;
5375
5774
  this.isUploading.set(true);
5376
5775
  this.uploadProgress.set(0);
5377
- try {
5378
- const command = {
5379
- conversationId: conversation.id,
5776
+ const command = {
5777
+ conversationId: conversation.id,
5778
+ type: 'voice',
5779
+ payload: {
5380
5780
  type: 'voice',
5381
- payload: {
5382
- type: 'voice',
5383
- url: '',
5384
- duration: this.recordingDuration(),
5385
- size: audioBlob.size,
5386
- mimeType: 'audio/webm',
5387
- },
5388
- };
5389
- await this.composerService.sendMessage(command, {
5390
- media: {
5391
- source: audioBlob,
5392
- fileName: `voice-${Date.now()}.webm`,
5393
- mimeType: 'audio/webm',
5394
- onProgress: (percent) => {
5395
- if (!uploadSignal.aborted) {
5396
- this.uploadProgress.set(percent);
5397
- }
5398
- },
5399
- signal: uploadSignal,
5781
+ url: '',
5782
+ duration: this.recordingDuration(),
5783
+ size: audioBlob.size,
5784
+ mimeType: 'audio/webm',
5785
+ },
5786
+ };
5787
+ this.composerService.hideComponent();
5788
+ void this.composerService
5789
+ .sendMessage(command, {
5790
+ media: {
5791
+ source: audioBlob,
5792
+ fileName: `voice-${Date.now()}.webm`,
5793
+ mimeType: 'audio/webm',
5794
+ onProgress: (percent) => {
5795
+ if (!uploadSignal.aborted) {
5796
+ this.uploadProgress.set(percent);
5797
+ }
5400
5798
  },
5401
- });
5402
- }
5403
- catch (error) {
5799
+ signal: uploadSignal,
5800
+ },
5801
+ })
5802
+ .catch((error) => {
5404
5803
  if (!isUploadAborted(error, uploadSignal)) {
5405
5804
  console.error('Failed to send voice message:', error);
5406
5805
  }
5407
- }
5408
- finally {
5806
+ })
5807
+ .finally(() => {
5409
5808
  this.uploadAbortController = undefined;
5410
5809
  if (!uploadSignal.aborted) {
5411
5810
  this.isUploading.set(false);
5412
5811
  this.uploadProgress.set(0);
5413
5812
  }
5414
- }
5813
+ });
5415
5814
  }
5416
5815
  stopAndCleanup() {
5417
5816
  this.abortInProgressUploads();
@@ -5986,282 +6385,67 @@ class AXBaseRegistry {
5986
6385
  update(id, updates) {
5987
6386
  this._items.update((items) => items.map((item) => (item.id === id ? { ...item, ...updates } : item)));
5988
6387
  }
5989
- /**
5990
- * Clear all items
5991
- */
5992
- clear() {
5993
- this._items.set([]);
5994
- }
5995
- /**
5996
- * Get count of registered items
5997
- */
5998
- get count() {
5999
- return this._items().length;
6000
- }
6001
- /**
6002
- * Get count of enabled items
6003
- */
6004
- get enabledCount() {
6005
- return this.enabledItems().length;
6006
- }
6007
- /**
6008
- * Validate item before registration
6009
- * Override in child classes for custom validation
6010
- */
6011
- validateItem(item) {
6012
- if (!item.id) {
6013
- throw new Error(`${this.getRegistryName()}: Item must have an id`);
6014
- }
6015
- }
6016
- }
6017
-
6018
- /**
6019
- * Conversation Item Action Registry
6020
- * Manages actions available in the conversation list item context menu
6021
- */
6022
- /**
6023
- * Conversation Item Action Registry Service
6024
- * Manages available actions in the conversation list item context menu
6025
- */
6026
- class AXConversationItemActionRegistry extends AXBaseRegistry {
6027
- constructor() {
6028
- super();
6029
- this.translation = inject(AXTranslationService);
6030
- this.injector = inject(Injector);
6031
- // Register default actions from individual token
6032
- inject(DEFAULT_CONVERSATION_ITEM_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6033
- // Register actions from REGISTRY_CONFIG
6034
- const config = inject(REGISTRY_CONFIG, { optional: true });
6035
- config?.conversationItemActions?.forEach((action) => this.register(action));
6036
- }
6037
- /**
6038
- * Get registry name for logging
6039
- */
6040
- getRegistryName() {
6041
- return 'ConversationItemActionRegistry';
6042
- }
6043
- /**
6044
- * Get actions for a specific conversation
6045
- */
6046
- getActionsForConversation(conversation) {
6047
- const context = { conversation };
6048
- return this.enabledItems()
6049
- .filter((action) => {
6050
- // Filter by visibility
6051
- if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6052
- return false;
6053
- }
6054
- return true;
6055
- })
6056
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6057
- }
6058
- isActionEnabled(action, context) {
6059
- if (action.enabled === false) {
6060
- return false;
6061
- }
6062
- if (action.enabledWhen) {
6063
- return runInInjectionContext(this.injector, () => action.enabledWhen?.(context)) !== false;
6064
- }
6065
- return true;
6066
- }
6067
- /**
6068
- * Get label for an action
6069
- */
6070
- getActionLabel(action, conversation) {
6071
- if (typeof action.label === 'function') {
6072
- const labelFactory = action.label;
6073
- return this.translation.translateSync(runInInjectionContext(this.injector, () => labelFactory({ conversation })));
6074
- }
6075
- return this.translation.translateSync(action.label);
6076
- }
6077
- /**
6078
- * Get icon for an action
6079
- */
6080
- getActionIcon(action, conversation) {
6081
- if (!action.icon)
6082
- return undefined;
6083
- if (typeof action.icon === 'function') {
6084
- const iconFactory = action.icon;
6085
- return runInInjectionContext(this.injector, () => iconFactory({ conversation }));
6086
- }
6087
- return action.icon;
6088
- }
6089
- /**
6090
- * Get tooltip for an action
6091
- */
6092
- getActionTooltip(action, conversation) {
6093
- if (!action.tooltip)
6094
- return undefined;
6095
- if (typeof action.tooltip === 'function') {
6096
- const tooltipFactory = action.tooltip;
6097
- return this.translation.translateSync(runInInjectionContext(this.injector, () => tooltipFactory({ conversation })));
6098
- }
6099
- return this.translation.translateSync(action.tooltip);
6100
- }
6101
- async executeAction(actionId, context) {
6102
- const action = this.getById(actionId);
6103
- if (!action) {
6104
- throw new Error(`Action "${actionId}" not found`);
6105
- }
6106
- if (!this.isActionEnabled(action, context)) {
6107
- throw new Error(`Action "${actionId}" is not enabled`);
6108
- }
6109
- if (action.handler) {
6110
- await runInInjectionContext(this.injector, () => action.handler?.(context));
6111
- }
6112
- }
6113
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6114
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry }); }
6115
- }
6116
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry, decorators: [{
6117
- type: Injectable
6118
- }], ctorParameters: () => [] });
6119
-
6120
- /**
6121
- * Conversation Tab Registry
6122
- * Register custom tabs for filtering conversations in the sidebar
6123
- */
6124
- /**
6125
- * Conversation Tab Registry Service
6126
- */
6127
- class AXConversationTabRegistry {
6128
- constructor() {
6129
- this._tabs = signal([], ...(ngDevMode ? [{ debugName: "_tabs" }] : []));
6130
- this.translation = inject(AXTranslationService);
6131
- /** All registered tabs */
6132
- this.tabs = this._tabs.asReadonly();
6133
- /** Enabled tabs sorted by priority */
6134
- this.enabledTabs = computed(() => [...this._tabs()].filter((tab) => tab.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledTabs" }] : []));
6135
- /** Default active tab */
6136
- this.defaultTab = computed(() => this.enabledTabs().find((tab) => tab.default === true) || this.enabledTabs()[0], ...(ngDevMode ? [{ debugName: "defaultTab" }] : []));
6137
- // Register default tabs from individual token
6138
- inject(DEFAULT_CONVERSATION_TABS, { optional: true })?.forEach((tab) => this.register(tab));
6139
- // Register tabs from REGISTRY_CONFIG
6140
- const config = inject(REGISTRY_CONFIG, { optional: true });
6141
- config?.conversationTabs?.forEach((tab) => this.register(tab));
6142
- }
6143
- /**
6144
- * Register a conversation tab
6145
- */
6146
- register(tab) {
6147
- // Validate tab
6148
- if (!tab.id) {
6149
- throw new Error('Conversation tab must have an id');
6150
- }
6151
- if (!tab.label) {
6152
- throw new Error('Conversation tab must have a label');
6153
- }
6154
- if (!tab.filter) {
6155
- throw new Error('Conversation tab must have a filter function');
6156
- }
6157
- // Check for duplicates
6158
- const existing = this._tabs().find((t) => t.id === tab.id);
6159
- if (existing) {
6160
- console.warn(`Conversation tab "${tab.id}" is already registered. Replacing...`);
6161
- this.unregister(tab.id);
6162
- }
6163
- // Add to registry
6164
- this._tabs.update((tabs) => [...tabs, tab]);
6165
- }
6166
- /**
6167
- * Unregister a conversation tab
6168
- */
6169
- unregister(id) {
6170
- this._tabs.update((tabs) => tabs.filter((t) => t.id !== id));
6171
- }
6172
- /**
6173
- * Get tab by ID
6174
- */
6175
- getTabById(id) {
6176
- return this._tabs().find((t) => t.id === id);
6177
- }
6178
- getTabLabel(tab) {
6179
- return this.translation.translateSync(tab.label);
6180
- }
6181
- /**
6182
- * Apply tab filter to conversations
6183
- */
6184
- filterConversations(tabId, conversations) {
6185
- const tab = this.getTabById(tabId);
6186
- if (!tab) {
6187
- throw new Error(`Tab "${tabId}" not found`);
6188
- }
6189
- return tab.filter(conversations);
6190
- }
6191
- /**
6192
- * Get badge for a tab
6193
- */
6194
- getBadge(tabId, conversations) {
6195
- const tab = this.getTabById(tabId);
6196
- if (!tab || !tab.badge) {
6197
- return undefined;
6198
- }
6199
- return tab.badge(conversations);
6388
+ /**
6389
+ * Clear all items
6390
+ */
6391
+ clear() {
6392
+ this._items.set([]);
6200
6393
  }
6201
6394
  /**
6202
- * Enable/disable a tab
6395
+ * Get count of registered items
6203
6396
  */
6204
- setEnabled(id, enabled) {
6205
- this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, enabled } : tab)));
6397
+ get count() {
6398
+ return this._items().length;
6206
6399
  }
6207
6400
  /**
6208
- * Set default tab
6401
+ * Get count of enabled items
6209
6402
  */
6210
- setDefault(id) {
6211
- this._tabs.update((tabs) => tabs.map((tab) => ({
6212
- ...tab,
6213
- default: tab.id === id,
6214
- })));
6403
+ get enabledCount() {
6404
+ return this.enabledItems().length;
6215
6405
  }
6216
6406
  /**
6217
- * Clear all tabs
6407
+ * Validate item before registration
6408
+ * Override in child classes for custom validation
6218
6409
  */
6219
- clear() {
6220
- this._tabs.set([]);
6410
+ validateItem(item) {
6411
+ if (!item.id) {
6412
+ throw new Error(`${this.getRegistryName()}: Item must have an id`);
6413
+ }
6221
6414
  }
6222
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6223
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry }); }
6224
6415
  }
6225
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry, decorators: [{
6226
- type: Injectable
6227
- }], ctorParameters: () => [] });
6228
6416
 
6229
6417
  /**
6230
- * Info Bar Action Registry
6231
- * Manages actions available in the conversation info bar
6418
+ * Conversation Item Action Registry
6419
+ * Manages actions available in the conversation list item context menu
6232
6420
  */
6233
6421
  /**
6234
- * Info Bar Action Registry Service
6235
- * Manages available actions in the conversation info bar
6422
+ * Conversation Item Action Registry Service
6423
+ * Manages available actions in the conversation list item context menu
6236
6424
  */
6237
- class AXInfoBarActionRegistry extends AXBaseRegistry {
6425
+ class AXConversationItemActionRegistry extends AXBaseRegistry {
6238
6426
  constructor() {
6239
6427
  super();
6240
6428
  this.translation = inject(AXTranslationService);
6241
6429
  this.injector = inject(Injector);
6242
6430
  // Register default actions from individual token
6243
- inject(DEFAULT_INFO_BAR_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6431
+ inject(DEFAULT_CONVERSATION_ITEM_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6244
6432
  // Register actions from REGISTRY_CONFIG
6245
6433
  const config = inject(REGISTRY_CONFIG, { optional: true });
6246
- config?.infoBarActions?.forEach((action) => this.register(action));
6434
+ config?.conversationItemActions?.forEach((action) => this.register(action));
6247
6435
  }
6248
6436
  /**
6249
6437
  * Get registry name for logging
6250
6438
  */
6251
6439
  getRegistryName() {
6252
- return 'InfoBarActionRegistry';
6440
+ return 'ConversationItemActionRegistry';
6253
6441
  }
6254
6442
  /**
6255
6443
  * Get actions for a specific conversation
6256
6444
  */
6257
- getActionsForConversation(conversation, location) {
6445
+ getActionsForConversation(conversation) {
6258
6446
  const context = { conversation };
6259
6447
  return this.enabledItems()
6260
6448
  .filter((action) => {
6261
- // Filter by location if specified
6262
- if (location && action.location && action.location !== location) {
6263
- return false;
6264
- }
6265
6449
  // Filter by visibility
6266
6450
  if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6267
6451
  return false;
@@ -6270,18 +6454,6 @@ class AXInfoBarActionRegistry extends AXBaseRegistry {
6270
6454
  })
6271
6455
  .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6272
6456
  }
6273
- /**
6274
- * Get inline actions (toolbar buttons)
6275
- */
6276
- getInlineActions(conversation) {
6277
- return this.getActionsForConversation(conversation, 'inline');
6278
- }
6279
- /**
6280
- * Get menu actions (dropdown items)
6281
- */
6282
- getMenuActions(conversation) {
6283
- return this.getActionsForConversation(conversation, 'menu');
6284
- }
6285
6457
  isActionEnabled(action, context) {
6286
6458
  if (action.enabled === false) {
6287
6459
  return false;
@@ -6325,653 +6497,575 @@ class AXInfoBarActionRegistry extends AXBaseRegistry {
6325
6497
  }
6326
6498
  return this.translation.translateSync(action.tooltip);
6327
6499
  }
6328
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6329
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry }); }
6330
- }
6331
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry, decorators: [{
6332
- type: Injectable
6333
- }], ctorParameters: () => [] });
6334
-
6335
- /**
6336
- * Message Action Registry
6337
- * Register custom actions that can be performed on messages
6338
- */
6339
- /**
6340
- * Message Action Registry Service
6341
- */
6342
- class AXMessageActionRegistry {
6343
- constructor() {
6344
- this._actions = signal([], ...(ngDevMode ? [{ debugName: "_actions" }] : []));
6345
- this.translation = inject(AXTranslationService);
6346
- this.injector = inject(Injector);
6347
- /** All registered actions */
6348
- this.actions = this._actions.asReadonly();
6349
- /** Actions sorted by priority */
6350
- this.sortedActions = computed(() => [...this._actions()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "sortedActions" }] : []));
6351
- // Register default actions from individual token
6352
- const defaultActions = inject(DEFAULT_MESSAGE_ACTIONS, { optional: true });
6353
- if (defaultActions) {
6354
- defaultActions.forEach((action) => this.register(action));
6355
- }
6356
- // Register actions from REGISTRY_CONFIG
6357
- const config = inject(REGISTRY_CONFIG, { optional: true });
6358
- config?.messageActions?.forEach((action) => this.register(action));
6359
- }
6360
- /**
6361
- * Register a message action
6362
- */
6363
- register(action) {
6364
- // Validate action
6365
- if (!action.id) {
6366
- throw new Error('Message action must have an id');
6367
- }
6368
- if (!action.label) {
6369
- throw new Error('Message action must have a label');
6370
- }
6371
- if (!action.handler) {
6372
- throw new Error('Message action must have a handler');
6373
- }
6374
- // Check for duplicates
6375
- const existing = this._actions().find((a) => a.id === action.id);
6376
- if (existing) {
6377
- console.warn(`Message action "${action.id}" is already registered. Replacing...`);
6378
- this.unregister(action.id);
6379
- }
6380
- // Add to registry
6381
- this._actions.update((actions) => [...actions, action]);
6382
- }
6383
- /**
6384
- * Unregister a message action
6385
- */
6386
- unregister(id) {
6387
- this._actions.update((actions) => actions.filter((a) => a.id !== id));
6388
- }
6389
- /**
6390
- * Get all actions for a specific message
6391
- */
6392
- getActions(message, currentUser) {
6393
- return this.sortedActions().filter((action) => runInInjectionContext(this.injector, () => action.visible(message, currentUser)) &&
6394
- runInInjectionContext(this.injector, () => action.enabled(message, currentUser)));
6395
- }
6396
- /**
6397
- * Get menu actions for a message
6398
- */
6399
- getMenuActions(message, currentUser) {
6400
- return this.getActions(message, currentUser);
6401
- }
6402
- /** Whether an action is enabled (safe to call outside an injection context). */
6403
- isActionEnabled(action, message, currentUser) {
6404
- return runInInjectionContext(this.injector, () => action.enabled(message, currentUser));
6405
- }
6406
- getActionLabel(action) {
6407
- return this.translation.translateSync(action.label);
6408
- }
6409
- /**
6410
- * Get inline actions for a message
6411
- */
6412
- getInlineActions(message, currentUser) {
6413
- return this.getActions(message, currentUser).filter((action) => action.showInline === true);
6414
- }
6415
- /**
6416
- * Get action by ID
6417
- */
6418
- getActionById(id) {
6419
- return this._actions().find((a) => a.id === id);
6420
- }
6421
- /**
6422
- * Execute an action
6423
- */
6424
6500
  async executeAction(actionId, context) {
6425
- const action = this.getActionById(actionId);
6501
+ const action = this.getById(actionId);
6426
6502
  if (!action) {
6427
6503
  throw new Error(`Action "${actionId}" not found`);
6428
6504
  }
6429
- // Check if enabled
6430
- if (!runInInjectionContext(this.injector, () => action.enabled(context.message, context.currentUser))) {
6505
+ if (!this.isActionEnabled(action, context)) {
6431
6506
  throw new Error(`Action "${actionId}" is not enabled`);
6432
6507
  }
6433
- // Execute handler
6434
- await runInInjectionContext(this.injector, () => action.handler(context));
6435
- }
6436
- /**
6437
- * Clear all actions
6438
- */
6439
- clear() {
6440
- this._actions.set([]);
6508
+ if (action.handler) {
6509
+ await runInInjectionContext(this.injector, () => action.handler?.(context));
6510
+ }
6441
6511
  }
6442
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6443
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry }); }
6512
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6513
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry }); }
6444
6514
  }
6445
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry, decorators: [{
6515
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry, decorators: [{
6446
6516
  type: Injectable
6447
6517
  }], ctorParameters: () => [] });
6448
6518
 
6449
6519
  /**
6450
- * IndexedDB Storage Wrapper
6451
- * Provides persistent storage for conversation data
6520
+ * Conversation Tab Registry
6521
+ * Register custom tabs for filtering conversations in the sidebar
6452
6522
  */
6453
- const DB_NAME = 'acorex-conversation-db';
6454
- const DB_VERSION = 2;
6455
- // Store names
6456
- const AXConversationIndexedDbStores = {
6457
- PARTICIPANTS: 'participants',
6458
- CONVERSATIONS: 'conversations',
6459
- MESSAGES: 'messages',
6460
- MEDIA: 'media',
6461
- SETTINGS: 'settings',
6462
- };
6463
- class AXConversationIndexedDbStorage {
6523
+ /**
6524
+ * Conversation Tab Registry Service
6525
+ */
6526
+ class AXConversationTabRegistry {
6464
6527
  constructor() {
6465
- this.db = null;
6466
- this.initPromise = null;
6467
- }
6468
- /**
6469
- * Initialize IndexedDB
6470
- */
6471
- async init() {
6472
- if (this.db)
6473
- return;
6474
- if (this.initPromise)
6475
- return this.initPromise;
6476
- this.initPromise = new Promise((resolve, reject) => {
6477
- const request = indexedDB.open(DB_NAME, DB_VERSION);
6478
- request.onerror = () => reject(request.error);
6479
- request.onsuccess = () => {
6480
- this.db = request.result;
6481
- resolve();
6482
- };
6483
- request.onupgradeneeded = (event) => {
6484
- const db = event.target.result;
6485
- // Create object stores if they don't exist
6486
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.PARTICIPANTS)) {
6487
- db.createObjectStore(AXConversationIndexedDbStores.PARTICIPANTS, { keyPath: 'id' });
6488
- }
6489
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.CONVERSATIONS)) {
6490
- const convStore = db.createObjectStore(AXConversationIndexedDbStores.CONVERSATIONS, { keyPath: 'id' });
6491
- convStore.createIndex('lastMessageAt', 'lastMessageAt', { unique: false });
6492
- }
6493
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MESSAGES)) {
6494
- const msgStore = db.createObjectStore(AXConversationIndexedDbStores.MESSAGES, { keyPath: 'id' });
6495
- msgStore.createIndex('conversationId', 'conversationId', { unique: false });
6496
- msgStore.createIndex('timestamp', 'timestamp', { unique: false });
6497
- }
6498
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.SETTINGS)) {
6499
- db.createObjectStore(AXConversationIndexedDbStores.SETTINGS, { keyPath: 'key' });
6500
- }
6501
- if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MEDIA)) {
6502
- db.createObjectStore(AXConversationIndexedDbStores.MEDIA, { keyPath: 'id' });
6503
- }
6504
- };
6505
- });
6506
- return this.initPromise;
6528
+ this._tabs = signal([], ...(ngDevMode ? [{ debugName: "_tabs" }] : []));
6529
+ this.translation = inject(AXTranslationService);
6530
+ /** All registered tabs */
6531
+ this.tabs = this._tabs.asReadonly();
6532
+ /** Enabled tabs sorted by priority */
6533
+ this.enabledTabs = computed(() => [...this._tabs()].filter((tab) => tab.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledTabs" }] : []));
6534
+ /** Default active tab */
6535
+ this.defaultTab = computed(() => this.enabledTabs().find((tab) => tab.default === true) || this.enabledTabs()[0], ...(ngDevMode ? [{ debugName: "defaultTab" }] : []));
6536
+ // Register default tabs from individual token
6537
+ inject(DEFAULT_CONVERSATION_TABS, { optional: true })?.forEach((tab) => this.register(tab));
6538
+ // Register tabs from REGISTRY_CONFIG
6539
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6540
+ config?.conversationTabs?.forEach((tab) => this.register(tab));
6507
6541
  }
6508
6542
  /**
6509
- * Get a value from a store
6543
+ * Register a conversation tab
6510
6544
  */
6511
- async get(storeName, key) {
6512
- await this.init();
6513
- if (!this.db)
6514
- throw new Error('Database not initialized');
6515
- const db = this.db;
6516
- return new Promise((resolve, reject) => {
6517
- const transaction = db.transaction(storeName, 'readonly');
6518
- const store = transaction.objectStore(storeName);
6519
- const request = store.get(key);
6520
- request.onsuccess = () => resolve(request.result);
6521
- request.onerror = () => reject(request.error);
6522
- });
6545
+ register(tab) {
6546
+ // Validate tab
6547
+ if (!tab.id) {
6548
+ throw new Error('Conversation tab must have an id');
6549
+ }
6550
+ if (!tab.label) {
6551
+ throw new Error('Conversation tab must have a label');
6552
+ }
6553
+ if (!tab.filter) {
6554
+ throw new Error('Conversation tab must have a filter function');
6555
+ }
6556
+ // Check for duplicates
6557
+ const existing = this._tabs().find((t) => t.id === tab.id);
6558
+ if (existing) {
6559
+ console.warn(`Conversation tab "${tab.id}" is already registered. Replacing...`);
6560
+ this.unregister(tab.id);
6561
+ }
6562
+ // Add to registry
6563
+ this._tabs.update((tabs) => [...tabs, tab]);
6523
6564
  }
6524
6565
  /**
6525
- * Get all values from a store
6566
+ * Unregister a conversation tab
6526
6567
  */
6527
- async getAll(storeName) {
6528
- await this.init();
6529
- if (!this.db)
6530
- throw new Error('Database not initialized');
6531
- const db = this.db;
6532
- return new Promise((resolve, reject) => {
6533
- const transaction = db.transaction(storeName, 'readonly');
6534
- const store = transaction.objectStore(storeName);
6535
- const request = store.getAll();
6536
- request.onsuccess = () => resolve(request.result);
6537
- request.onerror = () => reject(request.error);
6538
- });
6568
+ unregister(id) {
6569
+ this._tabs.update((tabs) => tabs.filter((t) => t.id !== id));
6539
6570
  }
6540
6571
  /**
6541
- * Get values by index
6572
+ * Get tab by ID
6542
6573
  */
6543
- async getAllByIndex(storeName, indexName, query) {
6544
- await this.init();
6545
- if (!this.db)
6546
- throw new Error('Database not initialized');
6547
- const db = this.db;
6548
- return new Promise((resolve, reject) => {
6549
- const transaction = db.transaction(storeName, 'readonly');
6550
- const store = transaction.objectStore(storeName);
6551
- const index = store.index(indexName);
6552
- const request = index.getAll(query);
6553
- request.onsuccess = () => resolve(request.result);
6554
- request.onerror = () => reject(request.error);
6555
- });
6574
+ getTabById(id) {
6575
+ return this._tabs().find((t) => t.id === id);
6576
+ }
6577
+ getTabLabel(tab) {
6578
+ return this.translation.translateSync(tab.label);
6556
6579
  }
6557
6580
  /**
6558
- * Put a value into a store
6581
+ * Apply tab filter to conversations
6559
6582
  */
6560
- async put(storeName, value) {
6561
- await this.init();
6562
- if (!this.db)
6563
- throw new Error('Database not initialized');
6564
- const db = this.db;
6565
- return new Promise((resolve, reject) => {
6566
- const transaction = db.transaction(storeName, 'readwrite');
6567
- const store = transaction.objectStore(storeName);
6568
- const request = store.put(value);
6569
- request.onsuccess = () => resolve();
6570
- request.onerror = () => reject(request.error);
6571
- });
6583
+ filterConversations(tabId, conversations) {
6584
+ const tab = this.getTabById(tabId);
6585
+ if (!tab) {
6586
+ throw new Error(`Tab "${tabId}" not found`);
6587
+ }
6588
+ return tab.filter(conversations);
6572
6589
  }
6573
6590
  /**
6574
- * Delete a value from a store
6591
+ * Get badge for a tab
6575
6592
  */
6576
- async delete(storeName, key) {
6577
- await this.init();
6578
- if (!this.db)
6579
- throw new Error('Database not initialized');
6580
- const db = this.db;
6581
- return new Promise((resolve, reject) => {
6582
- const transaction = db.transaction(storeName, 'readwrite');
6583
- const store = transaction.objectStore(storeName);
6584
- const request = store.delete(key);
6585
- request.onsuccess = () => resolve();
6586
- request.onerror = () => reject(request.error);
6587
- });
6593
+ getBadge(tabId, conversations) {
6594
+ const tab = this.getTabById(tabId);
6595
+ if (!tab || !tab.badge) {
6596
+ return undefined;
6597
+ }
6598
+ return tab.badge(conversations);
6588
6599
  }
6589
6600
  /**
6590
- * Clear all data from a store
6601
+ * Enable/disable a tab
6591
6602
  */
6592
- async clear(storeName) {
6593
- await this.init();
6594
- if (!this.db)
6595
- throw new Error('Database not initialized');
6596
- const db = this.db;
6597
- return new Promise((resolve, reject) => {
6598
- const transaction = db.transaction(storeName, 'readwrite');
6599
- const store = transaction.objectStore(storeName);
6600
- const request = store.clear();
6601
- request.onsuccess = () => resolve();
6602
- request.onerror = () => reject(request.error);
6603
- });
6604
- }
6605
- // =====================
6606
- // Convenience Methods
6607
- // =====================
6608
- async getParticipant(id) {
6609
- return this.get(AXConversationIndexedDbStores.PARTICIPANTS, id);
6610
- }
6611
- async getAllParticipants() {
6612
- return this.getAll(AXConversationIndexedDbStores.PARTICIPANTS);
6613
- }
6614
- async putParticipant(participant) {
6615
- return this.put(AXConversationIndexedDbStores.PARTICIPANTS, participant);
6616
- }
6617
- async getConversation(id) {
6618
- return this.get(AXConversationIndexedDbStores.CONVERSATIONS, id);
6619
- }
6620
- async getAllConversations() {
6621
- return this.getAll(AXConversationIndexedDbStores.CONVERSATIONS);
6622
- }
6623
- async putConversation(conversation) {
6624
- return this.put(AXConversationIndexedDbStores.CONVERSATIONS, conversation);
6625
- }
6626
- async deleteConversation(id) {
6627
- return this.delete(AXConversationIndexedDbStores.CONVERSATIONS, id);
6628
- }
6629
- async getMessage(id) {
6630
- return this.get(AXConversationIndexedDbStores.MESSAGES, id);
6631
- }
6632
- async getAllMessages() {
6633
- return this.getAll(AXConversationIndexedDbStores.MESSAGES);
6634
- }
6635
- async getMessagesByConversation(conversationId) {
6636
- return this.getAllByIndex(AXConversationIndexedDbStores.MESSAGES, 'conversationId', conversationId);
6637
- }
6638
- async putMessage(message) {
6639
- return this.put(AXConversationIndexedDbStores.MESSAGES, message);
6640
- }
6641
- async deleteMessage(id) {
6642
- return this.delete(AXConversationIndexedDbStores.MESSAGES, id);
6603
+ setEnabled(id, enabled) {
6604
+ this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, enabled } : tab)));
6643
6605
  }
6644
- async getSetting(key) {
6645
- const result = await this.get(AXConversationIndexedDbStores.SETTINGS, key);
6646
- return result?.value;
6606
+ /**
6607
+ * Set default tab
6608
+ */
6609
+ setDefault(id) {
6610
+ this._tabs.update((tabs) => tabs.map((tab) => ({
6611
+ ...tab,
6612
+ default: tab.id === id,
6613
+ })));
6647
6614
  }
6648
- async putSetting(key, value) {
6649
- return this.put(AXConversationIndexedDbStores.SETTINGS, { key, value });
6615
+ /**
6616
+ * Clear all tabs
6617
+ */
6618
+ clear() {
6619
+ this._tabs.set([]);
6650
6620
  }
6651
- async getMedia(id) {
6652
- return this.get(AXConversationIndexedDbStores.MEDIA, id);
6621
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6622
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry }); }
6623
+ }
6624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry, decorators: [{
6625
+ type: Injectable
6626
+ }], ctorParameters: () => [] });
6627
+
6628
+ /**
6629
+ * Info Bar Action Registry
6630
+ * Manages actions available in the conversation info bar
6631
+ */
6632
+ /**
6633
+ * Info Bar Action Registry Service
6634
+ * Manages available actions in the conversation info bar
6635
+ */
6636
+ class AXInfoBarActionRegistry extends AXBaseRegistry {
6637
+ constructor() {
6638
+ super();
6639
+ this.translation = inject(AXTranslationService);
6640
+ this.injector = inject(Injector);
6641
+ // Register default actions from individual token
6642
+ inject(DEFAULT_INFO_BAR_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6643
+ // Register actions from REGISTRY_CONFIG
6644
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6645
+ config?.infoBarActions?.forEach((action) => this.register(action));
6653
6646
  }
6654
- async putMedia(record) {
6655
- return this.put(AXConversationIndexedDbStores.MEDIA, record);
6647
+ /**
6648
+ * Get registry name for logging
6649
+ */
6650
+ getRegistryName() {
6651
+ return 'InfoBarActionRegistry';
6656
6652
  }
6657
- async deleteMedia(id) {
6658
- return this.delete(AXConversationIndexedDbStores.MEDIA, id);
6653
+ /**
6654
+ * Get actions for a specific conversation
6655
+ */
6656
+ getActionsForConversation(conversation, location) {
6657
+ const context = { conversation };
6658
+ return this.enabledItems()
6659
+ .filter((action) => {
6660
+ // Filter by location if specified
6661
+ if (location && action.location && action.location !== location) {
6662
+ return false;
6663
+ }
6664
+ // Filter by visibility
6665
+ if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6666
+ return false;
6667
+ }
6668
+ return true;
6669
+ })
6670
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6671
+ }
6672
+ /**
6673
+ * Get inline actions (toolbar buttons)
6674
+ */
6675
+ getInlineActions(conversation) {
6676
+ return this.getActionsForConversation(conversation, 'inline');
6659
6677
  }
6660
- }
6661
- // Export singleton instance
6662
- const axConversationIndexedDbStorage = new AXConversationIndexedDbStorage();
6663
-
6664
- var indexeddbStorage = /*#__PURE__*/Object.freeze({
6665
- __proto__: null,
6666
- AXConversationIndexedDbStorage: AXConversationIndexedDbStorage,
6667
- AXConversationIndexedDbStores: AXConversationIndexedDbStores,
6668
- axConversationIndexedDbStorage: axConversationIndexedDbStorage
6669
- });
6670
-
6671
- class AXConversationMessageUtilsService {
6672
6678
  /**
6673
- * Normalize optional avatar/icon values so empty or whitespace-only strings
6674
- * are treated as missing data.
6679
+ * Get menu actions (dropdown items)
6675
6680
  */
6676
- static normalizeOptionalMediaValue(value) {
6677
- if (typeof value !== 'string') {
6678
- return undefined;
6681
+ getMenuActions(conversation) {
6682
+ return this.getActionsForConversation(conversation, 'menu');
6683
+ }
6684
+ isActionEnabled(action, context) {
6685
+ if (action.enabled === false) {
6686
+ return false;
6679
6687
  }
6680
- const normalized = value.trim();
6681
- return normalized.length > 0 ? normalized : undefined;
6688
+ if (action.enabledWhen) {
6689
+ return runInInjectionContext(this.injector, () => action.enabledWhen?.(context)) !== false;
6690
+ }
6691
+ return true;
6682
6692
  }
6683
6693
  /**
6684
- * Get conversation avatar image URL.
6694
+ * Get label for an action
6685
6695
  */
6686
- static getConversationAvatar(conversation) {
6687
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.avatar);
6696
+ getActionLabel(action, conversation) {
6697
+ if (typeof action.label === 'function') {
6698
+ const labelFactory = action.label;
6699
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => labelFactory({ conversation })));
6700
+ }
6701
+ return this.translation.translateSync(action.label);
6688
6702
  }
6689
6703
  /**
6690
- * Font Awesome icon class(es) for a conversation when there is no avatar image.
6704
+ * Get icon for an action
6691
6705
  */
6692
- static getConversationAvatarIcon(conversation) {
6693
- if (AXConversationMessageUtilsService.getConversationAvatar(conversation)) {
6706
+ getActionIcon(action, conversation) {
6707
+ if (!action.icon)
6694
6708
  return undefined;
6709
+ if (typeof action.icon === 'function') {
6710
+ const iconFactory = action.icon;
6711
+ return runInInjectionContext(this.injector, () => iconFactory({ conversation }));
6695
6712
  }
6696
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.icon);
6713
+ return action.icon;
6697
6714
  }
6698
6715
  /**
6699
- * Get sender name from message
6716
+ * Get tooltip for an action
6700
6717
  */
6701
- static getSenderName(message, conversation) {
6702
- const participant = conversation.participants.find((p) => p.id === message.senderId);
6703
- return participant?.name || translateSync('@acorex:chat.fallbacks.unknown-user');
6718
+ getActionTooltip(action, conversation) {
6719
+ if (!action.tooltip)
6720
+ return undefined;
6721
+ if (typeof action.tooltip === 'function') {
6722
+ const tooltipFactory = action.tooltip;
6723
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => tooltipFactory({ conversation })));
6724
+ }
6725
+ return this.translation.translateSync(action.tooltip);
6726
+ }
6727
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6728
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry }); }
6729
+ }
6730
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry, decorators: [{
6731
+ type: Injectable
6732
+ }], ctorParameters: () => [] });
6733
+
6734
+ /**
6735
+ * Message Action Registry
6736
+ * Register custom actions that can be performed on messages
6737
+ */
6738
+ /**
6739
+ * Message Action Registry Service
6740
+ */
6741
+ class AXMessageActionRegistry {
6742
+ constructor() {
6743
+ this._actions = signal([], ...(ngDevMode ? [{ debugName: "_actions" }] : []));
6744
+ this.translation = inject(AXTranslationService);
6745
+ this.injector = inject(Injector);
6746
+ /** All registered actions */
6747
+ this.actions = this._actions.asReadonly();
6748
+ /** Actions sorted by priority */
6749
+ this.sortedActions = computed(() => [...this._actions()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "sortedActions" }] : []));
6750
+ // Register default actions from individual token
6751
+ const defaultActions = inject(DEFAULT_MESSAGE_ACTIONS, { optional: true });
6752
+ if (defaultActions) {
6753
+ defaultActions.forEach((action) => this.register(action));
6754
+ }
6755
+ // Register actions from REGISTRY_CONFIG
6756
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6757
+ config?.messageActions?.forEach((action) => this.register(action));
6704
6758
  }
6705
6759
  /**
6706
- * Get sender avatar image URL (takes precedence over {@link getSenderAvatarIcon}).
6760
+ * Register a message action
6707
6761
  */
6708
- static getSenderAvatar(message, conversation) {
6709
- const participant = conversation.participants.find((p) => p.id === message.senderId);
6710
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar);
6762
+ register(action) {
6763
+ // Validate action
6764
+ if (!action.id) {
6765
+ throw new Error('Message action must have an id');
6766
+ }
6767
+ if (!action.label) {
6768
+ throw new Error('Message action must have a label');
6769
+ }
6770
+ if (!action.handler) {
6771
+ throw new Error('Message action must have a handler');
6772
+ }
6773
+ // Check for duplicates
6774
+ const existing = this._actions().find((a) => a.id === action.id);
6775
+ if (existing) {
6776
+ console.warn(`Message action "${action.id}" is already registered. Replacing...`);
6777
+ this.unregister(action.id);
6778
+ }
6779
+ // Add to registry
6780
+ this._actions.update((actions) => [...actions, action]);
6711
6781
  }
6712
6782
  /**
6713
- * Font Awesome icon class(es) for the sender when there is no avatar image:
6714
- * participant `icon` first, then conversation-level `icon`.
6783
+ * Unregister a message action
6715
6784
  */
6716
- static getSenderAvatarIcon(message, conversation) {
6717
- const participant = conversation.participants.find((p) => p.id === message.senderId);
6718
- if (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar)) {
6719
- return undefined;
6720
- }
6721
- return (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.icon) ??
6722
- AXConversationMessageUtilsService.getConversationAvatarIcon(conversation));
6785
+ unregister(id) {
6786
+ this._actions.update((actions) => actions.filter((a) => a.id !== id));
6723
6787
  }
6724
6788
  /**
6725
- * Get initials from name
6789
+ * Get all actions for a specific message
6726
6790
  */
6727
- static getInitials(name) {
6728
- if (!name)
6729
- return '?';
6730
- return name
6731
- .split(' ')
6732
- .map((n) => n[0])
6733
- .join('')
6734
- .toUpperCase()
6735
- .substring(0, 2);
6791
+ getActions(message, currentUser) {
6792
+ return this.sortedActions().filter((action) => runInInjectionContext(this.injector, () => action.visible(message, currentUser)) &&
6793
+ runInInjectionContext(this.injector, () => action.enabled(message, currentUser)));
6736
6794
  }
6737
6795
  /**
6738
- * Type guard for text payload
6796
+ * Get menu actions for a message
6739
6797
  */
6740
- static isTextPayload(payload) {
6741
- return 'text' in payload && typeof payload.text === 'string';
6798
+ getMenuActions(message, currentUser) {
6799
+ return this.getActions(message, currentUser);
6800
+ }
6801
+ /** Whether an action is enabled (safe to call outside an injection context). */
6802
+ isActionEnabled(action, message, currentUser) {
6803
+ return runInInjectionContext(this.injector, () => action.enabled(message, currentUser));
6804
+ }
6805
+ getActionLabel(action) {
6806
+ return this.translation.translateSync(action.label);
6742
6807
  }
6743
6808
  /**
6744
- * Get message text content
6809
+ * Get inline actions for a message
6745
6810
  */
6746
- static getMessageText(message) {
6747
- if (message.type === 'text' && AXConversationMessageUtilsService.isTextPayload(message.payload)) {
6748
- return message.payload.text;
6749
- }
6750
- return `[${message.type}]`;
6811
+ getInlineActions(message, currentUser) {
6812
+ return this.getActions(message, currentUser).filter((action) => action.showInline === true);
6751
6813
  }
6752
6814
  /**
6753
- * Format message preview text
6815
+ * Get action by ID
6754
6816
  */
6755
- static getPreviewText(message, maxLength = 50) {
6756
- // Handle different message types
6757
- switch (message.type) {
6758
- case 'text': {
6759
- const textPayload = message.payload;
6760
- const text = textPayload.text || '';
6761
- const truncatedText = text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
6762
- return {
6763
- value: truncatedText,
6764
- type: 'text',
6765
- icon: 'fa-light fa-message',
6766
- };
6767
- }
6768
- case 'image': {
6769
- const imagePayload = normalizeImagePayload(message.payload);
6770
- const first = imagePayload.images[0];
6771
- const label = imagePayload.caption?.trim() ||
6772
- (imagePayload.images && imagePayload.images.length > 1
6773
- ? `${imagePayload.images.length} images`
6774
- : '');
6775
- return {
6776
- value: label || first?.thumbnailUrl || first?.url || '',
6777
- type: 'image',
6778
- icon: 'fa-light fa-image',
6779
- };
6780
- }
6781
- case 'video': {
6782
- const videoPayload = normalizeVideoPayload(message.payload);
6783
- const first = videoPayload.videos[0];
6784
- const label = videoPayload.caption?.trim() ||
6785
- (videoPayload.videos.length > 1 ? `${videoPayload.videos.length} videos` : '');
6786
- return {
6787
- value: label || first?.url || '',
6788
- type: 'video',
6789
- icon: 'fa-light fa-video',
6790
- };
6791
- }
6792
- case 'audio': {
6793
- const audioPayload = normalizeAudioPayload(message.payload);
6794
- const names = audioPayload.audios.map((a) => a.title).filter(Boolean).join(', ');
6795
- const first = audioPayload.audios[0];
6796
- const label = audioPayload.caption?.trim() ||
6797
- (audioPayload.audios.length > 1 ? `${audioPayload.audios.length} audio files` : names);
6798
- return {
6799
- value: label || first?.url || '',
6800
- type: 'audio',
6801
- icon: 'fa-light fa-music',
6802
- };
6803
- }
6804
- case 'voice': {
6805
- const voicePayload = message.payload;
6806
- return {
6807
- value: voicePayload.url || '',
6808
- type: 'voice',
6809
- icon: 'fa-light fa-microphone',
6810
- };
6811
- }
6812
- case 'file': {
6813
- const filePayload = normalizeFilePayload(message.payload);
6814
- const names = filePayload.files.map((f) => f.name).join(', ');
6815
- const label = filePayload.caption?.trim() ||
6816
- (filePayload.files.length > 1 ? `${filePayload.files.length} files` : names);
6817
- return {
6818
- value: label || names,
6819
- type: 'file',
6820
- icon: 'fa-light fa-file',
6821
- };
6822
- }
6823
- case 'location': {
6824
- const locationPayload = message.payload;
6825
- return {
6826
- value: locationPayload.latitude && locationPayload.longitude
6827
- ? `${locationPayload.latitude},${locationPayload.longitude}`
6828
- : '',
6829
- type: 'location',
6830
- icon: 'fa-light fa-location-dot',
6831
- };
6832
- }
6833
- case 'sticker': {
6834
- const stickerPayload = message.payload;
6835
- return {
6836
- value: stickerPayload.url || '',
6837
- type: 'sticker',
6838
- icon: 'fa-light fa-face-smile',
6839
- };
6840
- }
6841
- default:
6842
- return {
6843
- value: '',
6844
- type: message.type,
6845
- icon: 'fa-light fa-message',
6846
- };
6817
+ getActionById(id) {
6818
+ return this._actions().find((a) => a.id === id);
6819
+ }
6820
+ /**
6821
+ * Execute an action
6822
+ */
6823
+ async executeAction(actionId, context) {
6824
+ const action = this.getActionById(actionId);
6825
+ if (!action) {
6826
+ throw new Error(`Action "${actionId}" not found`);
6827
+ }
6828
+ // Check if enabled
6829
+ if (!runInInjectionContext(this.injector, () => action.enabled(context.message, context.currentUser))) {
6830
+ throw new Error(`Action "${actionId}" is not enabled`);
6847
6831
  }
6832
+ // Execute handler
6833
+ await runInInjectionContext(this.injector, () => action.handler(context));
6848
6834
  }
6849
6835
  /**
6850
- * Check if message is from current user
6836
+ * Clear all actions
6851
6837
  */
6852
- static isOwnMessage(message, currentUserId) {
6853
- return message.senderId === currentUserId;
6838
+ clear() {
6839
+ this._actions.set([]);
6840
+ }
6841
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6842
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry }); }
6843
+ }
6844
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry, decorators: [{
6845
+ type: Injectable
6846
+ }], ctorParameters: () => [] });
6847
+
6848
+ /**
6849
+ * IndexedDB Storage Wrapper
6850
+ * Provides persistent storage for conversation data
6851
+ */
6852
+ const DB_NAME = 'acorex-conversation-db';
6853
+ const DB_VERSION = 2;
6854
+ // Store names
6855
+ const AXConversationIndexedDbStores = {
6856
+ PARTICIPANTS: 'participants',
6857
+ CONVERSATIONS: 'conversations',
6858
+ MESSAGES: 'messages',
6859
+ MEDIA: 'media',
6860
+ SETTINGS: 'settings',
6861
+ };
6862
+ class AXConversationIndexedDbStorage {
6863
+ constructor() {
6864
+ this.db = null;
6865
+ this.initPromise = null;
6854
6866
  }
6855
6867
  /**
6856
- * Get message status icon class (Font Awesome)
6868
+ * Initialize IndexedDB
6857
6869
  */
6858
- static getStatusIcon(message) {
6859
- switch (message.status) {
6860
- case 'sending':
6861
- return 'fa-light fa-clock';
6862
- case 'sent':
6863
- return 'fa-light fa-check';
6864
- case 'delivered':
6865
- return 'fa-light fa-check-double';
6866
- case 'read':
6867
- return 'fa-light fa-check-double';
6868
- case 'failed':
6869
- return 'fa-light fa-circle-exclamation';
6870
- default:
6871
- return 'fa-light fa-check';
6872
- }
6870
+ async init() {
6871
+ if (this.db)
6872
+ return;
6873
+ if (this.initPromise)
6874
+ return this.initPromise;
6875
+ this.initPromise = new Promise((resolve, reject) => {
6876
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
6877
+ request.onerror = () => reject(request.error);
6878
+ request.onsuccess = () => {
6879
+ this.db = request.result;
6880
+ resolve();
6881
+ };
6882
+ request.onupgradeneeded = (event) => {
6883
+ const db = event.target.result;
6884
+ // Create object stores if they don't exist
6885
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.PARTICIPANTS)) {
6886
+ db.createObjectStore(AXConversationIndexedDbStores.PARTICIPANTS, { keyPath: 'id' });
6887
+ }
6888
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.CONVERSATIONS)) {
6889
+ const convStore = db.createObjectStore(AXConversationIndexedDbStores.CONVERSATIONS, { keyPath: 'id' });
6890
+ convStore.createIndex('lastMessageAt', 'lastMessageAt', { unique: false });
6891
+ }
6892
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MESSAGES)) {
6893
+ const msgStore = db.createObjectStore(AXConversationIndexedDbStores.MESSAGES, { keyPath: 'id' });
6894
+ msgStore.createIndex('conversationId', 'conversationId', { unique: false });
6895
+ msgStore.createIndex('timestamp', 'timestamp', { unique: false });
6896
+ }
6897
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.SETTINGS)) {
6898
+ db.createObjectStore(AXConversationIndexedDbStores.SETTINGS, { keyPath: 'key' });
6899
+ }
6900
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MEDIA)) {
6901
+ db.createObjectStore(AXConversationIndexedDbStores.MEDIA, { keyPath: 'id' });
6902
+ }
6903
+ };
6904
+ });
6905
+ return this.initPromise;
6873
6906
  }
6874
6907
  /**
6875
- * Check if message should show avatar
6908
+ * Get a value from a store
6876
6909
  */
6877
- static shouldShowAvatar(message, previousMessage, conversation) {
6878
- // Always show avatar for group conversations
6879
- if (conversation.type === 'group' || conversation.type === 'channel') {
6880
- // Don't show if same sender as previous message within 5 minutes
6881
- if (previousMessage && previousMessage.senderId === message.senderId) {
6882
- const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
6883
- return timeDiff > 5 * 60 * 1000; // 5 minutes
6884
- }
6885
- return true;
6886
- }
6887
- return false;
6910
+ async get(storeName, key) {
6911
+ await this.init();
6912
+ if (!this.db)
6913
+ throw new Error('Database not initialized');
6914
+ const db = this.db;
6915
+ return new Promise((resolve, reject) => {
6916
+ const transaction = db.transaction(storeName, 'readonly');
6917
+ const store = transaction.objectStore(storeName);
6918
+ const request = store.get(key);
6919
+ request.onsuccess = () => resolve(request.result);
6920
+ request.onerror = () => reject(request.error);
6921
+ });
6888
6922
  }
6889
6923
  /**
6890
- * Group messages by sender for consecutive messages
6924
+ * Get all values from a store
6891
6925
  */
6892
- static shouldGroupWithPrevious(message, previousMessage) {
6893
- if (!previousMessage)
6894
- return false;
6895
- // Same sender
6896
- if (message.senderId !== previousMessage.senderId)
6897
- return false;
6898
- // Within 5 minutes
6899
- const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
6900
- return timeDiff < 5 * 60 * 1000;
6926
+ async getAll(storeName) {
6927
+ await this.init();
6928
+ if (!this.db)
6929
+ throw new Error('Database not initialized');
6930
+ const db = this.db;
6931
+ return new Promise((resolve, reject) => {
6932
+ const transaction = db.transaction(storeName, 'readonly');
6933
+ const store = transaction.objectStore(storeName);
6934
+ const request = store.getAll();
6935
+ request.onsuccess = () => resolve(request.result);
6936
+ request.onerror = () => reject(request.error);
6937
+ });
6901
6938
  }
6902
6939
  /**
6903
- * Get conversation status for avatar (private conversations only)
6940
+ * Get values by index
6904
6941
  */
6905
- static getConversationStatus(conversation) {
6906
- if (conversation.type === 'private') {
6907
- return conversation.status.presence;
6908
- }
6909
- return undefined;
6942
+ async getAllByIndex(storeName, indexName, query) {
6943
+ await this.init();
6944
+ if (!this.db)
6945
+ throw new Error('Database not initialized');
6946
+ const db = this.db;
6947
+ return new Promise((resolve, reject) => {
6948
+ const transaction = db.transaction(storeName, 'readonly');
6949
+ const store = transaction.objectStore(storeName);
6950
+ const index = store.index(indexName);
6951
+ const request = index.getAll(query);
6952
+ request.onsuccess = () => resolve(request.result);
6953
+ request.onerror = () => reject(request.error);
6954
+ });
6910
6955
  }
6911
6956
  /**
6912
- * Get typing indicator text for conversation
6957
+ * Put a value into a store
6913
6958
  */
6914
- static getTypingText(conversation) {
6915
- const typingUsers = conversation.status.typingUsers;
6916
- if (typingUsers.length === 0)
6917
- return '';
6918
- if (conversation.type === 'private') {
6919
- return translateSync('@acorex:chat.status.typing');
6920
- }
6921
- const firstUser = conversation.participants.find((p) => p.id === typingUsers[0]);
6922
- if (typingUsers.length === 1) {
6923
- return translateSync('@acorex:chat.status.user-is-typing', {
6924
- params: { userName: firstUser?.name || translateSync('@acorex:chat.fallbacks.someone') },
6925
- });
6926
- }
6927
- return translateSync('@acorex:chat.status.people-typing', { params: { count: typingUsers.length } });
6959
+ async put(storeName, value) {
6960
+ await this.init();
6961
+ if (!this.db)
6962
+ throw new Error('Database not initialized');
6963
+ const db = this.db;
6964
+ return new Promise((resolve, reject) => {
6965
+ const transaction = db.transaction(storeName, 'readwrite');
6966
+ const store = transaction.objectStore(storeName);
6967
+ const request = store.put(value);
6968
+ request.onsuccess = () => resolve();
6969
+ request.onerror = () => reject(request.error);
6970
+ });
6971
+ }
6972
+ /**
6973
+ * Delete a value from a store
6974
+ */
6975
+ async delete(storeName, key) {
6976
+ await this.init();
6977
+ if (!this.db)
6978
+ throw new Error('Database not initialized');
6979
+ const db = this.db;
6980
+ return new Promise((resolve, reject) => {
6981
+ const transaction = db.transaction(storeName, 'readwrite');
6982
+ const store = transaction.objectStore(storeName);
6983
+ const request = store.delete(key);
6984
+ request.onsuccess = () => resolve();
6985
+ request.onerror = () => reject(request.error);
6986
+ });
6987
+ }
6988
+ /**
6989
+ * Clear all data from a store
6990
+ */
6991
+ async clear(storeName) {
6992
+ await this.init();
6993
+ if (!this.db)
6994
+ throw new Error('Database not initialized');
6995
+ const db = this.db;
6996
+ return new Promise((resolve, reject) => {
6997
+ const transaction = db.transaction(storeName, 'readwrite');
6998
+ const store = transaction.objectStore(storeName);
6999
+ const request = store.clear();
7000
+ request.onsuccess = () => resolve();
7001
+ request.onerror = () => reject(request.error);
7002
+ });
7003
+ }
7004
+ // =====================
7005
+ // Convenience Methods
7006
+ // =====================
7007
+ async getParticipant(id) {
7008
+ return this.get(AXConversationIndexedDbStores.PARTICIPANTS, id);
7009
+ }
7010
+ async getAllParticipants() {
7011
+ return this.getAll(AXConversationIndexedDbStores.PARTICIPANTS);
7012
+ }
7013
+ async putParticipant(participant) {
7014
+ return this.put(AXConversationIndexedDbStores.PARTICIPANTS, participant);
7015
+ }
7016
+ async getConversation(id) {
7017
+ return this.get(AXConversationIndexedDbStores.CONVERSATIONS, id);
7018
+ }
7019
+ async getAllConversations() {
7020
+ return this.getAll(AXConversationIndexedDbStores.CONVERSATIONS);
7021
+ }
7022
+ async putConversation(conversation) {
7023
+ return this.put(AXConversationIndexedDbStores.CONVERSATIONS, conversation);
7024
+ }
7025
+ async deleteConversation(id) {
7026
+ return this.delete(AXConversationIndexedDbStores.CONVERSATIONS, id);
7027
+ }
7028
+ async getMessage(id) {
7029
+ return this.get(AXConversationIndexedDbStores.MESSAGES, id);
7030
+ }
7031
+ async getAllMessages() {
7032
+ return this.getAll(AXConversationIndexedDbStores.MESSAGES);
7033
+ }
7034
+ async getMessagesByConversation(conversationId) {
7035
+ return this.getAllByIndex(AXConversationIndexedDbStores.MESSAGES, 'conversationId', conversationId);
6928
7036
  }
6929
- /**
6930
- * Format last seen time
6931
- */
6932
- static formatLastSeen(date) {
6933
- const now = new Date();
6934
- const diff = now.getTime() - date.getTime();
6935
- const seconds = Math.floor(diff / 1000);
6936
- if (seconds < 60)
6937
- return translateSync('@acorex:chat.time.just-now');
6938
- if (seconds < 3600)
6939
- return translateSync('@acorex:chat.time.minutes-ago', { params: { count: Math.floor(seconds / 60) } });
6940
- if (seconds < 86400)
6941
- return translateSync('@acorex:chat.time.hours-ago', { params: { count: Math.floor(seconds / 3600) } });
6942
- if (seconds < 604800)
6943
- return translateSync('@acorex:chat.time.days-ago', { params: { count: Math.floor(seconds / 86400) } });
6944
- return date.toLocaleDateString();
7037
+ async putMessage(message) {
7038
+ return this.put(AXConversationIndexedDbStores.MESSAGES, message);
6945
7039
  }
6946
- /**
6947
- * Get conversation subtitle (status or member count)
6948
- */
6949
- static getConversationSubtitle(conversation) {
6950
- if (conversation.status.isTyping) {
6951
- return AXConversationMessageUtilsService.getTypingText(conversation);
6952
- }
6953
- switch (conversation.type) {
6954
- case 'private':
6955
- if (conversation.status.presence === 'online') {
6956
- return translateSync('@acorex:chat.status.online');
6957
- }
6958
- if (conversation.status.lastSeen) {
6959
- return translateSync('@acorex:chat.status.last-seen', {
6960
- params: { value: AXConversationMessageUtilsService.formatLastSeen(conversation.status.lastSeen) },
6961
- });
6962
- }
6963
- return translateSync('@acorex:chat.status.offline');
6964
- case 'group':
6965
- return translateSync('@acorex:chat.members.count', { params: { count: conversation.participants.length } });
6966
- case 'channel':
6967
- return translateSync('@acorex:chat.members.subscribers-count', { params: { count: conversation.participants.length } });
6968
- case 'bot':
6969
- return translateSync('@acorex:chat.bot');
6970
- default:
6971
- return '';
6972
- }
7040
+ async deleteMessage(id) {
7041
+ return this.delete(AXConversationIndexedDbStores.MESSAGES, id);
7042
+ }
7043
+ async getSetting(key) {
7044
+ const result = await this.get(AXConversationIndexedDbStores.SETTINGS, key);
7045
+ return result?.value;
7046
+ }
7047
+ async putSetting(key, value) {
7048
+ return this.put(AXConversationIndexedDbStores.SETTINGS, { key, value });
7049
+ }
7050
+ async getMedia(id) {
7051
+ return this.get(AXConversationIndexedDbStores.MEDIA, id);
7052
+ }
7053
+ async putMedia(record) {
7054
+ return this.put(AXConversationIndexedDbStores.MEDIA, record);
7055
+ }
7056
+ async deleteMedia(id) {
7057
+ return this.delete(AXConversationIndexedDbStores.MEDIA, id);
6973
7058
  }
6974
7059
  }
7060
+ // Export singleton instance
7061
+ const axConversationIndexedDbStorage = new AXConversationIndexedDbStorage();
7062
+
7063
+ var indexeddbStorage = /*#__PURE__*/Object.freeze({
7064
+ __proto__: null,
7065
+ AXConversationIndexedDbStorage: AXConversationIndexedDbStorage,
7066
+ AXConversationIndexedDbStores: AXConversationIndexedDbStores,
7067
+ axConversationIndexedDbStorage: axConversationIndexedDbStorage
7068
+ });
6975
7069
 
6976
7070
  /**
6977
7071
  * Shared read/write helpers for IndexedDB chat storage (in-memory + persistence).
@@ -8417,20 +8511,27 @@ class AXConversationIndexedDbConversationApi extends AXConversationApi {
8417
8511
  // Conversation CRUD
8418
8512
  // =====================
8419
8513
  async createConversation(data) {
8514
+ const creatorId = data.creatorId ?? conversationSharedStorage.currentUserId;
8515
+ const participantIds = new Set(data.participantIds);
8516
+ if (creatorId) {
8517
+ participantIds.add(creatorId);
8518
+ }
8420
8519
  const conversation = {
8421
8520
  id: `conv-${Date.now()}`,
8422
8521
  type: data.type,
8423
- title: data.title || (data.type === 'private' ? 'New Chat' : data.type === 'group' ? 'New Group' : 'New Channel'),
8522
+ creatorId,
8523
+ title: data.title ||
8524
+ (data.type === 'private' ? 'New Chat' : data.type === 'group' ? 'New Group' : 'New Channel'),
8424
8525
  avatar: data.avatar,
8425
8526
  description: data.description,
8426
- participants: data.participantIds.map((id) => {
8527
+ participants: [...participantIds].map((id) => {
8427
8528
  const participant = conversationSharedStorage.participants.get(id);
8428
8529
  if (!participant) {
8429
8530
  throw this.createError('PARTICIPANT_NOT_FOUND', `Participant ${id} not found`, 404);
8430
8531
  }
8431
8532
  return {
8432
8533
  ...participant,
8433
- role: id === conversationSharedStorage.currentUserId ? 'admin' : 'member',
8534
+ role: id === creatorId ? 'admin' : 'member',
8434
8535
  };
8435
8536
  }),
8436
8537
  lastMessageAt: new Date(),
@@ -10400,41 +10501,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
10400
10501
  `, 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));// transform: scale(1.05);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"] }]
10401
10502
  }] });
10402
10503
 
10403
- /** Other participant in a private chat (excludes the current user). */
10404
- function resolvePrivatePeerUserId(conversation, currentUserId) {
10405
- if (conversation.type !== 'private') {
10406
- return undefined;
10407
- }
10408
- const currentId = currentUserId ?? 'current-user';
10409
- return conversation.participants.find((participant) => participant.id !== currentId)?.id;
10410
- }
10411
- /** Whether `auto` kind should render a user avatar for this conversation. */
10412
- function shouldUseUserAvatarForConversation(conversation, currentUserId) {
10413
- return conversation.type === 'private' && !!resolvePrivatePeerUserId(conversation, currentUserId);
10414
- }
10415
- function resolveUserAvatarDisplay(userId, conversation, message) {
10416
- if (message && conversation) {
10417
- return {
10418
- name: AXConversationMessageUtilsService.getSenderName(message, conversation),
10419
- avatar: AXConversationMessageUtilsService.getSenderAvatar(message, conversation),
10420
- icon: AXConversationMessageUtilsService.getSenderAvatarIcon(message, conversation),
10421
- };
10422
- }
10423
- const participant = conversation?.participants.find((p) => p.id === userId);
10424
- return {
10425
- name: participant?.name ?? userId,
10426
- avatar: participant?.avatar,
10427
- icon: participant?.icon,
10428
- };
10429
- }
10430
- function resolveConversationAvatarDisplay(conversation) {
10431
- return {
10432
- name: conversation.title,
10433
- avatar: AXConversationMessageUtilsService.getConversationAvatar(conversation),
10434
- icon: AXConversationMessageUtilsService.getConversationAvatarIcon(conversation),
10435
- };
10436
- }
10437
-
10438
10504
  /**
10439
10505
  * Conversation avatar host — renders a registered custom component or built-in fallback.
10440
10506
  */
@@ -10519,7 +10585,7 @@ class AXConversationAvatarComponent {
10519
10585
  if (!conversation) {
10520
10586
  return { name: explicitName ?? '?', avatar: explicitAvatar, icon: explicitIcon };
10521
10587
  }
10522
- const resolved = resolveConversationAvatarDisplay(conversation);
10588
+ const resolved = resolveConversationAvatarDisplay(conversation, this.currentUserId());
10523
10589
  return {
10524
10590
  name: explicitName ?? resolved.name,
10525
10591
  avatar: explicitAvatar ?? resolved.avatar,
@@ -10670,6 +10736,58 @@ class AXConversationDateUtilsService {
10670
10736
  }
10671
10737
  }
10672
10738
 
10739
+ function toUploaderReference(item) {
10740
+ return {
10741
+ url: item.url,
10742
+ mediaId: item.mediaId,
10743
+ mimeType: item.mimeType,
10744
+ size: item.size,
10745
+ };
10746
+ }
10747
+
10748
+ /**
10749
+ * Resolves multimedia payload URLs for renderers (direct url or mediaId via uploader).
10750
+ */
10751
+ /**
10752
+ * Creates a signal of the resolved playback URL for a media reference.
10753
+ */
10754
+ function createResolvedMediaUrlSignal(reference) {
10755
+ const uploader = inject(AXUploaderService);
10756
+ const destroyRef = inject(DestroyRef);
10757
+ const resolvedUrl = signal(null, ...(ngDevMode ? [{ debugName: "resolvedUrl" }] : []));
10758
+ let abortController = null;
10759
+ effect(() => {
10760
+ const ref = reference();
10761
+ abortController?.abort();
10762
+ abortController = new AbortController();
10763
+ const trimmedUrl = ref?.url?.trim();
10764
+ if (!trimmedUrl && !ref?.mediaId) {
10765
+ resolvedUrl.set(null);
10766
+ return;
10767
+ }
10768
+ const directUrl = trimmedUrl && !isNonPersistableMediaUrl(trimmedUrl) ? trimmedUrl : undefined;
10769
+ if (!ref?.mediaId) {
10770
+ resolvedUrl.set(directUrl ?? null);
10771
+ return;
10772
+ }
10773
+ resolvedUrl.set(directUrl ?? null);
10774
+ void uploader
10775
+ .resolvePlaybackUrl(ref, abortController.signal)
10776
+ .then((url) => {
10777
+ if (!abortController?.signal.aborted) {
10778
+ resolvedUrl.set(url);
10779
+ }
10780
+ })
10781
+ .catch(() => {
10782
+ if (!abortController?.signal.aborted && directUrl) {
10783
+ resolvedUrl.set(directUrl);
10784
+ }
10785
+ });
10786
+ });
10787
+ destroyRef.onDestroy(() => abortController?.abort());
10788
+ return resolvedUrl.asReadonly();
10789
+ }
10790
+
10673
10791
  function isMessageDeliveryPending(status) {
10674
10792
  return status === 'sending';
10675
10793
  }
@@ -10868,15 +10986,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
10868
10986
  `, 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"] }]
10869
10987
  }] });
10870
10988
 
10871
- function toUploaderReference(item) {
10872
- return {
10873
- url: item.url,
10874
- mediaId: item.mediaId,
10875
- mimeType: item.mimeType,
10876
- size: item.size,
10877
- };
10878
- }
10879
-
10880
10989
  /** Shared formatting for message renderer UIs. */
10881
10990
  function formatMediaDuration(seconds) {
10882
10991
  if (!Number.isFinite(seconds)) {
@@ -10896,49 +11005,6 @@ function formatFileByteSize(bytes) {
10896
11005
  return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
10897
11006
  }
10898
11007
 
10899
- /**
10900
- * Resolves multimedia payload URLs for renderers (direct url or mediaId via uploader).
10901
- */
10902
- /**
10903
- * Creates a signal of the resolved playback URL for a media reference.
10904
- */
10905
- function createResolvedMediaUrlSignal(reference) {
10906
- const uploader = inject(AXUploaderService);
10907
- const destroyRef = inject(DestroyRef);
10908
- const resolvedUrl = signal(null, ...(ngDevMode ? [{ debugName: "resolvedUrl" }] : []));
10909
- let abortController = null;
10910
- effect(() => {
10911
- const ref = reference();
10912
- abortController?.abort();
10913
- abortController = new AbortController();
10914
- const trimmedUrl = ref?.url?.trim();
10915
- if (!trimmedUrl && !ref?.mediaId) {
10916
- resolvedUrl.set(null);
10917
- return;
10918
- }
10919
- const directUrl = trimmedUrl && !isNonPersistableMediaUrl(trimmedUrl) ? trimmedUrl : undefined;
10920
- if (!ref?.mediaId) {
10921
- resolvedUrl.set(directUrl ?? null);
10922
- return;
10923
- }
10924
- resolvedUrl.set(directUrl ?? null);
10925
- void uploader
10926
- .resolvePlaybackUrl(ref, abortController.signal)
10927
- .then((url) => {
10928
- if (!abortController?.signal.aborted) {
10929
- resolvedUrl.set(url);
10930
- }
10931
- })
10932
- .catch(() => {
10933
- if (!abortController?.signal.aborted && directUrl) {
10934
- resolvedUrl.set(directUrl);
10935
- }
10936
- });
10937
- });
10938
- destroyRef.onDestroy(() => abortController?.abort());
10939
- return resolvedUrl.asReadonly();
10940
- }
10941
-
10942
11008
  class AXAudioAttachmentComponent {
10943
11009
  constructor() {
10944
11010
  this.item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
@@ -13013,6 +13079,18 @@ async function copyWithFileTypeFallback(message, registry, fallback) {
13013
13079
  const result = await fileType.copy(message.payload);
13014
13080
  return resolveFileCopyText(result)?.trim() || fallback();
13015
13081
  }
13082
+ /** Use file-type `open` on the message when `message.fileType` is set. */
13083
+ async function openWithFileType(message, registry, ctx) {
13084
+ if (!message.fileType) {
13085
+ return false;
13086
+ }
13087
+ const fileType = await registry.get(message.fileType);
13088
+ if (!fileType?.open) {
13089
+ return false;
13090
+ }
13091
+ const result = await runFileTypeOpen(fileType, ctx, message.payload);
13092
+ return result !== false;
13093
+ }
13016
13094
  /**
13017
13095
  * Registers the live renderer on {@link AXMessageListService} for message actions.
13018
13096
  */
@@ -14923,14 +15001,23 @@ class AXConversationService {
14923
15001
  this._messageDeleted$ = new Subject();
14924
15002
  this._typingIndicator$ = new Subject();
14925
15003
  this._presenceUpdate$ = new Subject();
14926
- /** All conversations */
14927
- this.conversations = this.state.conversations;
15004
+ /** All conversations (viewer-scoped display titles for private 1v1 chats) */
15005
+ this.conversations = computed(() => {
15006
+ const currentUserId = this._currentUser()?.id;
15007
+ return this.state.conversations().map((conversation) => resolveConversationForViewer(conversation, currentUserId));
15008
+ }, ...(ngDevMode ? [{ debugName: "conversations" }] : []));
14928
15009
  /** Active conversation ID */
14929
15010
  this.activeConversationId = this._activeConversationId.asReadonly();
14930
15011
  /** Active conversation */
14931
15012
  this.activeConversation = computed(() => {
14932
15013
  const id = this._activeConversationId();
14933
- return id ? this.state.getConversation(id) : null;
15014
+ if (!id) {
15015
+ return null;
15016
+ }
15017
+ const conversation = this.state.getConversation(id);
15018
+ return conversation
15019
+ ? resolveConversationForViewer(conversation, this._currentUser()?.id)
15020
+ : null;
14934
15021
  }, ...(ngDevMode ? [{ debugName: "activeConversation" }] : []));
14935
15022
  /** Messages for active conversation */
14936
15023
  this.activeMessages = computed(() => {
@@ -15721,17 +15808,29 @@ class AXConversationService {
15721
15808
  */
15722
15809
  async createConversation(participantIds, type, metadata) {
15723
15810
  try {
15811
+ const currentUser = this._currentUser();
15812
+ const creatorId = currentUser?.id;
15813
+ const isPrivate = type === 'private';
15814
+ const scopedParticipantIds = [...participantIds];
15815
+ if (creatorId && !scopedParticipantIds.includes(creatorId)) {
15816
+ scopedParticipantIds.unshift(creatorId);
15817
+ }
15724
15818
  const conversation = await this.conversationApi.createConversation({
15725
15819
  type,
15726
- participantIds,
15727
- title: metadata?.['title'],
15820
+ participantIds: scopedParticipantIds,
15821
+ creatorId,
15822
+ title: isPrivate ? undefined : metadata?.['title'],
15728
15823
  description: metadata?.['description'],
15729
- avatar: AXConversationService.normalizeOptionalString(metadata?.['avatar']),
15730
- icon: AXConversationService.normalizeOptionalString(metadata?.['icon']),
15731
- metadata,
15824
+ avatar: isPrivate
15825
+ ? undefined
15826
+ : AXConversationService.normalizeOptionalString(metadata?.['avatar']),
15827
+ icon: isPrivate
15828
+ ? undefined
15829
+ : AXConversationService.normalizeOptionalString(metadata?.['icon']),
15830
+ metadata: isPrivate ? undefined : metadata,
15732
15831
  });
15733
15832
  this.state.setConversation(conversation);
15734
- return conversation;
15833
+ return resolveConversationForViewer(conversation, creatorId);
15735
15834
  }
15736
15835
  catch (error) {
15737
15836
  this.errorHandler.handle(error, 'createConversation', { participantIds, type });
@@ -16437,8 +16536,6 @@ class AXComposerComponent {
16437
16536
  this.composerPopupComponent = AXComposerPopupComponent;
16438
16537
  /** Input placeholder - use service */
16439
16538
  this.placeholder = this.composerService.placeholder;
16440
- /** Sending message state */
16441
- this.sendingMessage = signal(false, ...(ngDevMode ? [{ debugName: "sendingMessage" }] : []));
16442
16539
  /** Typing indicator subject for rate limiting */
16443
16540
  this.typingSubject = new Subject();
16444
16541
  // Focus text area when requested via AXComposerService.focusComposer()
@@ -16565,51 +16662,44 @@ class AXComposerComponent {
16565
16662
  }
16566
16663
  /** Handle send click */
16567
16664
  async onSendClick() {
16568
- if (!this.canSend() || this.sendingMessage())
16665
+ if (!this.canSend())
16569
16666
  return;
16570
16667
  const conversation = this.activeConversation();
16571
16668
  if (!conversation)
16572
16669
  return;
16573
16670
  const text = this.draftText().trim();
16574
16671
  const editingMsg = this.editingMessage();
16575
- this.sendingMessage.set(true);
16576
- try {
16577
- // Check if we're editing a message
16578
- if (editingMsg) {
16579
- // Edit existing message
16672
+ if (editingMsg) {
16673
+ try {
16580
16674
  await this.conversationService.editMessage(editingMsg.id, { type: 'text', text });
16581
- }
16582
- else {
16583
- // Create send command for new message
16584
- const command = {
16585
- conversationId: conversation.id,
16586
- type: 'text',
16587
- payload: { type: 'text', text },
16588
- replyToId: this.replyingToMessage()?.id,
16589
- };
16590
- // Send new message
16591
- await this.composerService.sendMessage(command);
16592
- this.messageSent.emit(command);
16593
- // Request message list to scroll to bottom after sending
16594
- this.messageListService.requestScrollToBottom();
16595
- }
16596
- // Clear input and draft
16597
- this.draftText.set('');
16598
- this.attachments.set([]);
16599
- this.cancelEditReply();
16600
- this.composerService.clear();
16601
- // Clear saved draft after successful send
16602
- if (conversation.id) {
16675
+ this.draftText.set('');
16676
+ this.attachments.set([]);
16677
+ this.cancelEditReply();
16603
16678
  this.composerService.clearDraft(conversation.id);
16679
+ this.composerService.focusComposer();
16604
16680
  }
16605
- this.composerService.focusComposer();
16606
- }
16607
- catch (error) {
16608
- console.error('Failed to send/edit message:', error);
16609
- }
16610
- finally {
16611
- this.sendingMessage.set(false);
16681
+ catch (error) {
16682
+ console.error('Failed to edit message:', error);
16683
+ }
16684
+ return;
16612
16685
  }
16686
+ const command = {
16687
+ conversationId: conversation.id,
16688
+ type: 'text',
16689
+ payload: { type: 'text', text },
16690
+ replyToId: this.replyingToMessage()?.id,
16691
+ };
16692
+ this.editingMessage.set(null);
16693
+ this.replyingToMessage.set(null);
16694
+ void this.composerService
16695
+ .sendMessage(command)
16696
+ .then(() => {
16697
+ this.messageSent.emit(command);
16698
+ this.messageListService.requestScrollToBottom();
16699
+ })
16700
+ .catch((error) => {
16701
+ console.error('Failed to send message:', error);
16702
+ });
16613
16703
  }
16614
16704
  /** Handle emoji click */
16615
16705
  async onEmojiClick(event) {
@@ -16663,16 +16753,14 @@ class AXComposerComponent {
16663
16753
  },
16664
16754
  replyToId: this.replyingToMessage()?.id,
16665
16755
  };
16666
- try {
16667
- await this.composerService.sendMessage(command);
16668
- // Clear reply mode after sending
16669
- this.cancelEditReply();
16670
- // Request message list to scroll to bottom after sending sticker
16756
+ void this.composerService
16757
+ .sendMessage(command)
16758
+ .then(() => {
16671
16759
  this.messageListService.requestScrollToBottom();
16672
- }
16673
- catch (error) {
16760
+ })
16761
+ .catch((error) => {
16674
16762
  console.error('Failed to send sticker:', error);
16675
- }
16763
+ });
16676
16764
  }
16677
16765
  /** Get inputs for emoji popup component */
16678
16766
  getEmojiPopupInputs() {
@@ -20960,12 +21048,7 @@ class AXNewConversationDialogComponent extends AXBasePageComponent {
20960
21048
  try {
20961
21049
  let conversation;
20962
21050
  if (selectedIds.length === 1) {
20963
- const selectedUser = this.availableUsers().find((u) => u.id === selectedIds[0]);
20964
- const metadata = {
20965
- title: selectedUser?.name,
20966
- avatar: selectedUser?.avatar,
20967
- };
20968
- conversation = await this.conversationService.createConversation(selectedIds, 'private', metadata);
21051
+ conversation = await this.conversationService.createConversation(selectedIds, 'private');
20969
21052
  }
20970
21053
  else {
20971
21054
  const metadata = {
@@ -22179,5 +22262,5 @@ function getErrorMessage(code, params) {
22179
22262
  * Generated bundle index. Do not edit.
22180
22263
  */
22181
22264
 
22182
- 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 };
22265
+ 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 };
22183
22266
  //# sourceMappingURL=acorex-components-conversation2.mjs.map