@acorex/components 20.8.20 → 20.8.21

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,7 +7,7 @@ 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
13
  import { createFileTypeMetadata, AXFileTypeInfoProvider, formatFileSizeBytes, AXFileService, AXFileTypeRegistryService, provideFileValidationRules, provideFileTypeInfoProvider, resolveFileCopyText } from '@acorex/core/file';
@@ -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`.
@@ -5688,443 +6092,162 @@ class AXComposerActionRegistry {
5688
6092
  this.fileTypeRegistry = inject(AXFileTypeRegistryService);
5689
6093
  /** All registered actions */
5690
6094
  this.actions = this._actions.asReadonly();
5691
- /** Left-side actions sorted by priority */
5692
- this.leftActions = computed(() => [...this._actions()]
5693
- .filter((action) => action.position === 'left')
5694
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "leftActions" }] : []));
5695
- /** Right-side actions sorted by priority */
5696
- this.rightActions = computed(() => [...this._actions()]
5697
- .filter((action) => action.position === 'right')
5698
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "rightActions" }] : []));
5699
- /** Left-side inline actions (location = 'inline' or undefined) */
5700
- this.leftInlineActions = computed(() => this.leftActions().filter((action) => action.location !== 'menu'), ...(ngDevMode ? [{ debugName: "leftInlineActions" }] : []));
5701
- /** Left-side menu actions (location = 'menu') */
5702
- this.leftMenuActions = computed(() => this.leftActions().filter((action) => action.location === 'menu'), ...(ngDevMode ? [{ debugName: "leftMenuActions" }] : []));
5703
- /** Right-side inline actions (location = 'inline' or undefined) */
5704
- this.rightInlineActions = computed(() => this.rightActions().filter((action) => action.location !== 'menu'), ...(ngDevMode ? [{ debugName: "rightInlineActions" }] : []));
5705
- /** Right-side menu actions (location = 'menu') */
5706
- this.rightMenuActions = computed(() => this.rightActions().filter((action) => action.location === 'menu'), ...(ngDevMode ? [{ debugName: "rightMenuActions" }] : []));
5707
- inject(CONVERSATION_FILE_TYPES_READY);
5708
- // Register default actions from individual token
5709
- inject(DEFAULT_COMPOSER_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
5710
- // Register actions from REGISTRY_CONFIG
5711
- const config = inject(REGISTRY_CONFIG, { optional: true });
5712
- config?.composerActions?.forEach((action) => this.register(action));
5713
- }
5714
- /**
5715
- * Register a composer action
5716
- */
5717
- register(action) {
5718
- // Validate action
5719
- if (!action.id) {
5720
- throw new Error('Composer action must have an id');
5721
- }
5722
- if (!action.label) {
5723
- throw new Error('Composer action must have a label');
5724
- }
5725
- if (!action.icon) {
5726
- throw new Error('Composer action must have an icon');
5727
- }
5728
- if (!action.handler && !action.component) {
5729
- throw new Error('Composer action must have either a handler or a component');
5730
- }
5731
- // Check for duplicates
5732
- const existing = this._actions().find((a) => a.id === action.id);
5733
- if (existing) {
5734
- console.warn(`Composer action "${action.id}" is already registered. Replacing...`);
5735
- this.unregister(action.id);
5736
- }
5737
- // Add to registry
5738
- this._actions.update((actions) => [...actions, action]);
5739
- if (action.fileType) {
5740
- void this.ensureFileTypeExists(action.fileType);
5741
- }
5742
- }
5743
- async ensureFileTypeExists(fileType) {
5744
- const match = await this.fileTypeRegistry.get(fileType);
5745
- if (!match) {
5746
- console.warn(`[AXComposerActionRegistry] Unknown fileType "${fileType}". ` +
5747
- 'Register its AXFileTypeInfoProvider in app providers before using this action.');
5748
- }
5749
- }
5750
- /**
5751
- * Unregister a composer action
5752
- */
5753
- unregister(id) {
5754
- this._actions.update((actions) => actions.filter((a) => a.id !== id));
5755
- }
5756
- /**
5757
- * Get visible actions for a position
5758
- */
5759
- getVisibleActions(position, context) {
5760
- const actions = position === 'left' ? this.leftActions() : this.rightActions();
5761
- return actions.filter((action) => runInInjectionContext(this.injector, () => action.visible(context)));
5762
- }
5763
- /**
5764
- * Get enabled actions for a position
5765
- */
5766
- getEnabledActions(position, context) {
5767
- return this.getVisibleActions(position, context).filter((action) => runInInjectionContext(this.injector, () => action.enabled?.(context)) !== false);
5768
- }
5769
- isActionEnabled(action, context) {
5770
- return runInInjectionContext(this.injector, () => action.enabled?.(context)) !== false;
5771
- }
5772
- /**
5773
- * Picker actions for a file catalog (by priority), when visible and enabled.
5774
- */
5775
- findPickerActionsForFileType(catalogName, context) {
5776
- return [...this._actions()]
5777
- .filter((action) => action.component && action.fileType === catalogName)
5778
- .filter((action) => runInInjectionContext(this.injector, () => action.visible(context)))
5779
- .filter((action) => this.isActionEnabled(action, context))
5780
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
5781
- }
5782
- /**
5783
- * First picker action for a file catalog (by priority), when visible and enabled.
5784
- */
5785
- findPickerActionForFileType(catalogName, context) {
5786
- return this.findPickerActionsForFileType(catalogName, context)[0];
5787
- }
5788
- /**
5789
- * Get action by ID
5790
- */
5791
- getActionById(id) {
5792
- return this._actions().find((a) => a.id === id);
5793
- }
5794
- getActionLabel(action) {
5795
- return this.translation.translateSync(action.label);
5796
- }
5797
- getActionTooltip(action) {
5798
- if (!action.tooltip)
5799
- return undefined;
5800
- return this.translation.translateSync(action.tooltip);
5801
- }
5802
- /**
5803
- * Execute an action
5804
- */
5805
- async executeAction(actionId, context) {
5806
- const action = this.getActionById(actionId);
5807
- if (!action) {
5808
- throw new Error(`Action "${actionId}" not found`);
5809
- }
5810
- if (!runInInjectionContext(this.injector, () => action.visible(context))) {
5811
- throw new Error(`Action "${actionId}" is not visible`);
5812
- }
5813
- if (action.enabled && !runInInjectionContext(this.injector, () => action.enabled?.(context))) {
5814
- throw new Error(`Action "${actionId}" is not enabled`);
5815
- }
5816
- if (action.handler) {
5817
- await runInInjectionContext(this.injector, () => action.handler?.(context));
5818
- }
5819
- }
5820
- /**
5821
- * Update action badge
5822
- */
5823
- setBadge(id, badge) {
5824
- this._actions.update((actions) => actions.map((action) => (action.id === id ? { ...action, badge } : action)));
5825
- }
5826
- /**
5827
- * Clear all actions
5828
- */
5829
- clear() {
5830
- this._actions.set([]);
5831
- }
5832
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5833
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerActionRegistry }); }
5834
- }
5835
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerActionRegistry, decorators: [{
5836
- type: Injectable
5837
- }], ctorParameters: () => [] });
5838
-
5839
- /**
5840
- * Composer Tab Registry
5841
- * Register custom tabs for the composer popup (emoji, stickers, GIFs, etc.)
5842
- */
5843
- /**
5844
- * Composer Tab Registry Service
5845
- */
5846
- class AXComposerTabRegistry {
5847
- constructor() {
5848
- this._tabs = signal([], ...(ngDevMode ? [{ debugName: "_tabs" }] : []));
5849
- this.translation = inject(AXTranslationService);
5850
- /** All registered tabs */
5851
- this.tabs = this._tabs.asReadonly();
5852
- /** Enabled tabs sorted by priority */
5853
- this.enabledTabs = computed(() => [...this._tabs()].filter((tab) => tab.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledTabs" }] : []));
5854
- // Register default tabs from individual token
5855
- inject(DEFAULT_COMPOSER_TABS, { optional: true })?.forEach((tab) => this.register(tab));
5856
- // Register tabs from REGISTRY_CONFIG
5857
- const config = inject(REGISTRY_CONFIG, { optional: true });
5858
- config?.composerTabs?.forEach((tab) => this.register(tab));
5859
- }
5860
- /**
5861
- * Register a composer tab
5862
- */
5863
- register(tab) {
5864
- // Validate tab
5865
- if (!tab.id) {
5866
- throw new Error('Composer tab must have an id');
5867
- }
5868
- if (!tab.title) {
5869
- throw new Error('Composer tab must have a title');
5870
- }
5871
- if (!tab.component) {
5872
- throw new Error('Composer tab must have a component');
5873
- }
5874
- // Check for duplicates
5875
- const existing = this._tabs().find((t) => t.id === tab.id);
5876
- if (existing) {
5877
- console.warn(`Composer tab "${tab.id}" is already registered. Replacing...`);
5878
- this.unregister(tab.id);
5879
- }
5880
- // Add to registry
5881
- this._tabs.update((tabs) => [...tabs, tab]);
5882
- }
5883
- /**
5884
- * Unregister a composer tab
5885
- */
5886
- unregister(id) {
5887
- this._tabs.update((tabs) => tabs.filter((t) => t.id !== id));
5888
- }
5889
- /**
5890
- * Get tab by ID
5891
- */
5892
- getTabById(id) {
5893
- return this._tabs().find((t) => t.id === id);
5894
- }
5895
- getTabTitle(tab) {
5896
- return this.translation.translateSync(tab.title);
5897
- }
5898
- /**
5899
- * Enable/disable a tab
5900
- */
5901
- setEnabled(id, enabled) {
5902
- this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, enabled } : tab)));
5903
- }
5904
- /**
5905
- * Update tab badge
5906
- */
5907
- setBadge(id, badge) {
5908
- this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, badge } : tab)));
5909
- }
5910
- /**
5911
- * Clear all tabs
5912
- */
5913
- clear() {
5914
- this._tabs.set([]);
5915
- }
5916
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerTabRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5917
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerTabRegistry }); }
5918
- }
5919
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerTabRegistry, decorators: [{
5920
- type: Injectable
5921
- }], ctorParameters: () => [] });
5922
-
5923
- /**
5924
- * Abstract Base Registry
5925
- * Base class for all registries with common functionality
5926
- */
5927
- /**
5928
- * Abstract Base Registry Class
5929
- * Provides common registry functionality
5930
- */
5931
- class AXBaseRegistry {
5932
- constructor() {
5933
- this._items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : []));
5934
- /** All registered items */
5935
- this.items = this._items.asReadonly();
5936
- /** Enabled items sorted by priority */
5937
- this.enabledItems = computed(() => [...this._items()].filter((item) => item.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledItems" }] : []));
6095
+ /** Left-side actions sorted by priority */
6096
+ this.leftActions = computed(() => [...this._actions()]
6097
+ .filter((action) => action.position === 'left')
6098
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "leftActions" }] : []));
6099
+ /** Right-side actions sorted by priority */
6100
+ this.rightActions = computed(() => [...this._actions()]
6101
+ .filter((action) => action.position === 'right')
6102
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "rightActions" }] : []));
6103
+ /** Left-side inline actions (location = 'inline' or undefined) */
6104
+ this.leftInlineActions = computed(() => this.leftActions().filter((action) => action.location !== 'menu'), ...(ngDevMode ? [{ debugName: "leftInlineActions" }] : []));
6105
+ /** Left-side menu actions (location = 'menu') */
6106
+ this.leftMenuActions = computed(() => this.leftActions().filter((action) => action.location === 'menu'), ...(ngDevMode ? [{ debugName: "leftMenuActions" }] : []));
6107
+ /** Right-side inline actions (location = 'inline' or undefined) */
6108
+ this.rightInlineActions = computed(() => this.rightActions().filter((action) => action.location !== 'menu'), ...(ngDevMode ? [{ debugName: "rightInlineActions" }] : []));
6109
+ /** Right-side menu actions (location = 'menu') */
6110
+ this.rightMenuActions = computed(() => this.rightActions().filter((action) => action.location === 'menu'), ...(ngDevMode ? [{ debugName: "rightMenuActions" }] : []));
6111
+ inject(CONVERSATION_FILE_TYPES_READY);
6112
+ // Register default actions from individual token
6113
+ inject(DEFAULT_COMPOSER_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6114
+ // Register actions from REGISTRY_CONFIG
6115
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6116
+ config?.composerActions?.forEach((action) => this.register(action));
5938
6117
  }
5939
6118
  /**
5940
- * Register an item
6119
+ * Register a composer action
5941
6120
  */
5942
- register(item) {
5943
- this.validateItem(item);
6121
+ register(action) {
6122
+ // Validate action
6123
+ if (!action.id) {
6124
+ throw new Error('Composer action must have an id');
6125
+ }
6126
+ if (!action.label) {
6127
+ throw new Error('Composer action must have a label');
6128
+ }
6129
+ if (!action.icon) {
6130
+ throw new Error('Composer action must have an icon');
6131
+ }
6132
+ if (!action.handler && !action.component) {
6133
+ throw new Error('Composer action must have either a handler or a component');
6134
+ }
5944
6135
  // Check for duplicates
5945
- const existing = this._items().find((i) => i.id === item.id);
6136
+ const existing = this._actions().find((a) => a.id === action.id);
5946
6137
  if (existing) {
5947
- console.warn(`${this.getRegistryName()}: Item "${item.id}" is already registered. Replacing...`);
5948
- this.unregister(item.id);
6138
+ console.warn(`Composer action "${action.id}" is already registered. Replacing...`);
6139
+ this.unregister(action.id);
5949
6140
  }
5950
6141
  // Add to registry
5951
- this._items.update((items) => [...items, item]);
6142
+ this._actions.update((actions) => [...actions, action]);
6143
+ if (action.fileType) {
6144
+ void this.ensureFileTypeExists(action.fileType);
6145
+ }
5952
6146
  }
5953
- /**
5954
- * Register multiple items
5955
- */
5956
- registerMany(items) {
5957
- items.forEach((item) => this.register(item));
6147
+ async ensureFileTypeExists(fileType) {
6148
+ const match = await this.fileTypeRegistry.get(fileType);
6149
+ if (!match) {
6150
+ console.warn(`[AXComposerActionRegistry] Unknown fileType "${fileType}". ` +
6151
+ 'Register its AXFileTypeInfoProvider in app providers before using this action.');
6152
+ }
5958
6153
  }
5959
6154
  /**
5960
- * Unregister an item by ID
6155
+ * Unregister a composer action
5961
6156
  */
5962
6157
  unregister(id) {
5963
- this._items.update((items) => items.filter((i) => i.id !== id));
5964
- }
5965
- /**
5966
- * Get item by ID
5967
- */
5968
- getById(id) {
5969
- return this._items().find((i) => i.id === id);
5970
- }
5971
- /**
5972
- * Check if item exists
5973
- */
5974
- has(id) {
5975
- return this.getById(id) !== undefined;
5976
- }
5977
- /**
5978
- * Enable/disable an item
5979
- */
5980
- setEnabled(id, enabled) {
5981
- this._items.update((items) => items.map((item) => (item.id === id ? { ...item, enabled } : item)));
5982
- }
5983
- /**
5984
- * Update an item
5985
- */
5986
- update(id, updates) {
5987
- this._items.update((items) => items.map((item) => (item.id === id ? { ...item, ...updates } : item)));
6158
+ this._actions.update((actions) => actions.filter((a) => a.id !== id));
5988
6159
  }
5989
6160
  /**
5990
- * Clear all items
6161
+ * Get visible actions for a position
5991
6162
  */
5992
- clear() {
5993
- this._items.set([]);
6163
+ getVisibleActions(position, context) {
6164
+ const actions = position === 'left' ? this.leftActions() : this.rightActions();
6165
+ return actions.filter((action) => runInInjectionContext(this.injector, () => action.visible(context)));
5994
6166
  }
5995
6167
  /**
5996
- * Get count of registered items
6168
+ * Get enabled actions for a position
5997
6169
  */
5998
- get count() {
5999
- return this._items().length;
6170
+ getEnabledActions(position, context) {
6171
+ return this.getVisibleActions(position, context).filter((action) => runInInjectionContext(this.injector, () => action.enabled?.(context)) !== false);
6000
6172
  }
6001
- /**
6002
- * Get count of enabled items
6003
- */
6004
- get enabledCount() {
6005
- return this.enabledItems().length;
6173
+ isActionEnabled(action, context) {
6174
+ return runInInjectionContext(this.injector, () => action.enabled?.(context)) !== false;
6006
6175
  }
6007
6176
  /**
6008
- * Validate item before registration
6009
- * Override in child classes for custom validation
6177
+ * Picker actions for a file catalog (by priority), when visible and enabled.
6010
6178
  */
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));
6179
+ findPickerActionsForFileType(catalogName, context) {
6180
+ return [...this._actions()]
6181
+ .filter((action) => action.component && action.fileType === catalogName)
6182
+ .filter((action) => runInInjectionContext(this.injector, () => action.visible(context)))
6183
+ .filter((action) => this.isActionEnabled(action, context))
6184
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6036
6185
  }
6037
6186
  /**
6038
- * Get registry name for logging
6187
+ * First picker action for a file catalog (by priority), when visible and enabled.
6039
6188
  */
6040
- getRegistryName() {
6041
- return 'ConversationItemActionRegistry';
6189
+ findPickerActionForFileType(catalogName, context) {
6190
+ return this.findPickerActionsForFileType(catalogName, context)[0];
6042
6191
  }
6043
6192
  /**
6044
- * Get actions for a specific conversation
6193
+ * Get action by ID
6045
6194
  */
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;
6195
+ getActionById(id) {
6196
+ return this._actions().find((a) => a.id === id);
6066
6197
  }
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
- }
6198
+ getActionLabel(action) {
6075
6199
  return this.translation.translateSync(action.label);
6076
6200
  }
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) {
6201
+ getActionTooltip(action) {
6093
6202
  if (!action.tooltip)
6094
6203
  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
6204
  return this.translation.translateSync(action.tooltip);
6100
6205
  }
6206
+ /**
6207
+ * Execute an action
6208
+ */
6101
6209
  async executeAction(actionId, context) {
6102
- const action = this.getById(actionId);
6210
+ const action = this.getActionById(actionId);
6103
6211
  if (!action) {
6104
6212
  throw new Error(`Action "${actionId}" not found`);
6105
6213
  }
6106
- if (!this.isActionEnabled(action, context)) {
6214
+ if (!runInInjectionContext(this.injector, () => action.visible(context))) {
6215
+ throw new Error(`Action "${actionId}" is not visible`);
6216
+ }
6217
+ if (action.enabled && !runInInjectionContext(this.injector, () => action.enabled?.(context))) {
6107
6218
  throw new Error(`Action "${actionId}" is not enabled`);
6108
6219
  }
6109
6220
  if (action.handler) {
6110
6221
  await runInInjectionContext(this.injector, () => action.handler?.(context));
6111
6222
  }
6112
6223
  }
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 }); }
6224
+ /**
6225
+ * Update action badge
6226
+ */
6227
+ setBadge(id, badge) {
6228
+ this._actions.update((actions) => actions.map((action) => (action.id === id ? { ...action, badge } : action)));
6229
+ }
6230
+ /**
6231
+ * Clear all actions
6232
+ */
6233
+ clear() {
6234
+ this._actions.set([]);
6235
+ }
6236
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6237
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerActionRegistry }); }
6115
6238
  }
6116
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry, decorators: [{
6239
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerActionRegistry, decorators: [{
6117
6240
  type: Injectable
6118
6241
  }], ctorParameters: () => [] });
6119
6242
 
6120
6243
  /**
6121
- * Conversation Tab Registry
6122
- * Register custom tabs for filtering conversations in the sidebar
6244
+ * Composer Tab Registry
6245
+ * Register custom tabs for the composer popup (emoji, stickers, GIFs, etc.)
6123
6246
  */
6124
6247
  /**
6125
- * Conversation Tab Registry Service
6248
+ * Composer Tab Registry Service
6126
6249
  */
6127
- class AXConversationTabRegistry {
6250
+ class AXComposerTabRegistry {
6128
6251
  constructor() {
6129
6252
  this._tabs = signal([], ...(ngDevMode ? [{ debugName: "_tabs" }] : []));
6130
6253
  this.translation = inject(AXTranslationService);
@@ -6132,39 +6255,37 @@ class AXConversationTabRegistry {
6132
6255
  this.tabs = this._tabs.asReadonly();
6133
6256
  /** Enabled tabs sorted by priority */
6134
6257
  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
6258
  // Register default tabs from individual token
6138
- inject(DEFAULT_CONVERSATION_TABS, { optional: true })?.forEach((tab) => this.register(tab));
6259
+ inject(DEFAULT_COMPOSER_TABS, { optional: true })?.forEach((tab) => this.register(tab));
6139
6260
  // Register tabs from REGISTRY_CONFIG
6140
6261
  const config = inject(REGISTRY_CONFIG, { optional: true });
6141
- config?.conversationTabs?.forEach((tab) => this.register(tab));
6262
+ config?.composerTabs?.forEach((tab) => this.register(tab));
6142
6263
  }
6143
6264
  /**
6144
- * Register a conversation tab
6265
+ * Register a composer tab
6145
6266
  */
6146
6267
  register(tab) {
6147
6268
  // Validate tab
6148
6269
  if (!tab.id) {
6149
- throw new Error('Conversation tab must have an id');
6270
+ throw new Error('Composer tab must have an id');
6150
6271
  }
6151
- if (!tab.label) {
6152
- throw new Error('Conversation tab must have a label');
6272
+ if (!tab.title) {
6273
+ throw new Error('Composer tab must have a title');
6153
6274
  }
6154
- if (!tab.filter) {
6155
- throw new Error('Conversation tab must have a filter function');
6275
+ if (!tab.component) {
6276
+ throw new Error('Composer tab must have a component');
6156
6277
  }
6157
6278
  // Check for duplicates
6158
6279
  const existing = this._tabs().find((t) => t.id === tab.id);
6159
6280
  if (existing) {
6160
- console.warn(`Conversation tab "${tab.id}" is already registered. Replacing...`);
6281
+ console.warn(`Composer tab "${tab.id}" is already registered. Replacing...`);
6161
6282
  this.unregister(tab.id);
6162
6283
  }
6163
6284
  // Add to registry
6164
6285
  this._tabs.update((tabs) => [...tabs, tab]);
6165
6286
  }
6166
6287
  /**
6167
- * Unregister a conversation tab
6288
+ * Unregister a composer tab
6168
6289
  */
6169
6290
  unregister(id) {
6170
6291
  this._tabs.update((tabs) => tabs.filter((t) => t.id !== id));
@@ -6175,28 +6296,8 @@ class AXConversationTabRegistry {
6175
6296
  getTabById(id) {
6176
6297
  return this._tabs().find((t) => t.id === id);
6177
6298
  }
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);
6299
+ getTabTitle(tab) {
6300
+ return this.translation.translateSync(tab.title);
6200
6301
  }
6201
6302
  /**
6202
6303
  * Enable/disable a tab
@@ -6205,13 +6306,10 @@ class AXConversationTabRegistry {
6205
6306
  this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, enabled } : tab)));
6206
6307
  }
6207
6308
  /**
6208
- * Set default tab
6309
+ * Update tab badge
6209
6310
  */
6210
- setDefault(id) {
6211
- this._tabs.update((tabs) => tabs.map((tab) => ({
6212
- ...tab,
6213
- default: tab.id === id,
6214
- })));
6311
+ setBadge(id, badge) {
6312
+ this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, badge } : tab)));
6215
6313
  }
6216
6314
  /**
6217
6315
  * Clear all tabs
@@ -6219,759 +6317,760 @@ class AXConversationTabRegistry {
6219
6317
  clear() {
6220
6318
  this._tabs.set([]);
6221
6319
  }
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 }); }
6320
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerTabRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6321
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerTabRegistry }); }
6224
6322
  }
6225
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry, decorators: [{
6323
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXComposerTabRegistry, decorators: [{
6226
6324
  type: Injectable
6227
6325
  }], ctorParameters: () => [] });
6228
6326
 
6229
6327
  /**
6230
- * Info Bar Action Registry
6231
- * Manages actions available in the conversation info bar
6328
+ * Abstract Base Registry
6329
+ * Base class for all registries with common functionality
6232
6330
  */
6233
6331
  /**
6234
- * Info Bar Action Registry Service
6235
- * Manages available actions in the conversation info bar
6332
+ * Abstract Base Registry Class
6333
+ * Provides common registry functionality
6236
6334
  */
6237
- class AXInfoBarActionRegistry extends AXBaseRegistry {
6335
+ class AXBaseRegistry {
6238
6336
  constructor() {
6239
- super();
6240
- this.translation = inject(AXTranslationService);
6241
- this.injector = inject(Injector);
6242
- // Register default actions from individual token
6243
- inject(DEFAULT_INFO_BAR_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6244
- // Register actions from REGISTRY_CONFIG
6245
- const config = inject(REGISTRY_CONFIG, { optional: true });
6246
- config?.infoBarActions?.forEach((action) => this.register(action));
6337
+ this._items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : []));
6338
+ /** All registered items */
6339
+ this.items = this._items.asReadonly();
6340
+ /** Enabled items sorted by priority */
6341
+ this.enabledItems = computed(() => [...this._items()].filter((item) => item.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledItems" }] : []));
6247
6342
  }
6248
6343
  /**
6249
- * Get registry name for logging
6344
+ * Register an item
6250
6345
  */
6251
- getRegistryName() {
6252
- return 'InfoBarActionRegistry';
6346
+ register(item) {
6347
+ this.validateItem(item);
6348
+ // Check for duplicates
6349
+ const existing = this._items().find((i) => i.id === item.id);
6350
+ if (existing) {
6351
+ console.warn(`${this.getRegistryName()}: Item "${item.id}" is already registered. Replacing...`);
6352
+ this.unregister(item.id);
6353
+ }
6354
+ // Add to registry
6355
+ this._items.update((items) => [...items, item]);
6253
6356
  }
6254
6357
  /**
6255
- * Get actions for a specific conversation
6358
+ * Register multiple items
6256
6359
  */
6257
- getActionsForConversation(conversation, location) {
6258
- const context = { conversation };
6259
- return this.enabledItems()
6260
- .filter((action) => {
6261
- // Filter by location if specified
6262
- if (location && action.location && action.location !== location) {
6263
- return false;
6264
- }
6265
- // Filter by visibility
6266
- if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6267
- return false;
6268
- }
6269
- return true;
6270
- })
6271
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6360
+ registerMany(items) {
6361
+ items.forEach((item) => this.register(item));
6272
6362
  }
6273
6363
  /**
6274
- * Get inline actions (toolbar buttons)
6364
+ * Unregister an item by ID
6275
6365
  */
6276
- getInlineActions(conversation) {
6277
- return this.getActionsForConversation(conversation, 'inline');
6366
+ unregister(id) {
6367
+ this._items.update((items) => items.filter((i) => i.id !== id));
6278
6368
  }
6279
6369
  /**
6280
- * Get menu actions (dropdown items)
6370
+ * Get item by ID
6281
6371
  */
6282
- getMenuActions(conversation) {
6283
- return this.getActionsForConversation(conversation, 'menu');
6372
+ getById(id) {
6373
+ return this._items().find((i) => i.id === id);
6284
6374
  }
6285
- isActionEnabled(action, context) {
6286
- if (action.enabled === false) {
6287
- return false;
6288
- }
6289
- if (action.enabledWhen) {
6290
- return runInInjectionContext(this.injector, () => action.enabledWhen?.(context)) !== false;
6291
- }
6292
- return true;
6375
+ /**
6376
+ * Check if item exists
6377
+ */
6378
+ has(id) {
6379
+ return this.getById(id) !== undefined;
6293
6380
  }
6294
6381
  /**
6295
- * Get label for an action
6382
+ * Enable/disable an item
6296
6383
  */
6297
- getActionLabel(action, conversation) {
6298
- if (typeof action.label === 'function') {
6299
- const labelFactory = action.label;
6300
- return this.translation.translateSync(runInInjectionContext(this.injector, () => labelFactory({ conversation })));
6301
- }
6302
- return this.translation.translateSync(action.label);
6384
+ setEnabled(id, enabled) {
6385
+ this._items.update((items) => items.map((item) => (item.id === id ? { ...item, enabled } : item)));
6303
6386
  }
6304
6387
  /**
6305
- * Get icon for an action
6388
+ * Update an item
6306
6389
  */
6307
- getActionIcon(action, conversation) {
6308
- if (!action.icon)
6309
- return undefined;
6310
- if (typeof action.icon === 'function') {
6311
- const iconFactory = action.icon;
6312
- return runInInjectionContext(this.injector, () => iconFactory({ conversation }));
6313
- }
6314
- return action.icon;
6390
+ update(id, updates) {
6391
+ this._items.update((items) => items.map((item) => (item.id === id ? { ...item, ...updates } : item)));
6315
6392
  }
6316
6393
  /**
6317
- * Get tooltip for an action
6394
+ * Clear all items
6318
6395
  */
6319
- getActionTooltip(action, conversation) {
6320
- if (!action.tooltip)
6321
- return undefined;
6322
- if (typeof action.tooltip === 'function') {
6323
- const tooltipFactory = action.tooltip;
6324
- return this.translation.translateSync(runInInjectionContext(this.injector, () => tooltipFactory({ conversation })));
6396
+ clear() {
6397
+ this._items.set([]);
6398
+ }
6399
+ /**
6400
+ * Get count of registered items
6401
+ */
6402
+ get count() {
6403
+ return this._items().length;
6404
+ }
6405
+ /**
6406
+ * Get count of enabled items
6407
+ */
6408
+ get enabledCount() {
6409
+ return this.enabledItems().length;
6410
+ }
6411
+ /**
6412
+ * Validate item before registration
6413
+ * Override in child classes for custom validation
6414
+ */
6415
+ validateItem(item) {
6416
+ if (!item.id) {
6417
+ throw new Error(`${this.getRegistryName()}: Item must have an id`);
6325
6418
  }
6326
- return this.translation.translateSync(action.tooltip);
6327
6419
  }
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
6420
  }
6331
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry, decorators: [{
6332
- type: Injectable
6333
- }], ctorParameters: () => [] });
6334
6421
 
6335
6422
  /**
6336
- * Message Action Registry
6337
- * Register custom actions that can be performed on messages
6423
+ * Conversation Item Action Registry
6424
+ * Manages actions available in the conversation list item context menu
6338
6425
  */
6339
6426
  /**
6340
- * Message Action Registry Service
6427
+ * Conversation Item Action Registry Service
6428
+ * Manages available actions in the conversation list item context menu
6341
6429
  */
6342
- class AXMessageActionRegistry {
6430
+ class AXConversationItemActionRegistry extends AXBaseRegistry {
6343
6431
  constructor() {
6344
- this._actions = signal([], ...(ngDevMode ? [{ debugName: "_actions" }] : []));
6432
+ super();
6345
6433
  this.translation = inject(AXTranslationService);
6346
6434
  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
6435
  // 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
- }
6436
+ inject(DEFAULT_CONVERSATION_ITEM_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6356
6437
  // Register actions from REGISTRY_CONFIG
6357
6438
  const config = inject(REGISTRY_CONFIG, { optional: true });
6358
- config?.messageActions?.forEach((action) => this.register(action));
6439
+ config?.conversationItemActions?.forEach((action) => this.register(action));
6359
6440
  }
6360
6441
  /**
6361
- * Register a message action
6442
+ * Get registry name for logging
6362
6443
  */
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]);
6444
+ getRegistryName() {
6445
+ return 'ConversationItemActionRegistry';
6382
6446
  }
6383
6447
  /**
6384
- * Unregister a message action
6448
+ * Get actions for a specific conversation
6385
6449
  */
6386
- unregister(id) {
6387
- this._actions.update((actions) => actions.filter((a) => a.id !== id));
6450
+ getActionsForConversation(conversation) {
6451
+ const context = { conversation };
6452
+ return this.enabledItems()
6453
+ .filter((action) => {
6454
+ // Filter by visibility
6455
+ if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6456
+ return false;
6457
+ }
6458
+ return true;
6459
+ })
6460
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6388
6461
  }
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)));
6462
+ isActionEnabled(action, context) {
6463
+ if (action.enabled === false) {
6464
+ return false;
6465
+ }
6466
+ if (action.enabledWhen) {
6467
+ return runInInjectionContext(this.injector, () => action.enabledWhen?.(context)) !== false;
6468
+ }
6469
+ return true;
6395
6470
  }
6396
6471
  /**
6397
- * Get menu actions for a message
6472
+ * Get label for an action
6398
6473
  */
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) {
6474
+ getActionLabel(action, conversation) {
6475
+ if (typeof action.label === 'function') {
6476
+ const labelFactory = action.label;
6477
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => labelFactory({ conversation })));
6478
+ }
6407
6479
  return this.translation.translateSync(action.label);
6408
6480
  }
6409
6481
  /**
6410
- * Get inline actions for a message
6482
+ * Get icon for an action
6411
6483
  */
6412
- getInlineActions(message, currentUser) {
6413
- return this.getActions(message, currentUser).filter((action) => action.showInline === true);
6484
+ getActionIcon(action, conversation) {
6485
+ if (!action.icon)
6486
+ return undefined;
6487
+ if (typeof action.icon === 'function') {
6488
+ const iconFactory = action.icon;
6489
+ return runInInjectionContext(this.injector, () => iconFactory({ conversation }));
6490
+ }
6491
+ return action.icon;
6414
6492
  }
6415
6493
  /**
6416
- * Get action by ID
6494
+ * Get tooltip for an action
6417
6495
  */
6418
- getActionById(id) {
6419
- return this._actions().find((a) => a.id === id);
6496
+ getActionTooltip(action, conversation) {
6497
+ if (!action.tooltip)
6498
+ return undefined;
6499
+ if (typeof action.tooltip === 'function') {
6500
+ const tooltipFactory = action.tooltip;
6501
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => tooltipFactory({ conversation })));
6502
+ }
6503
+ return this.translation.translateSync(action.tooltip);
6420
6504
  }
6421
- /**
6422
- * Execute an action
6423
- */
6424
6505
  async executeAction(actionId, context) {
6425
- const action = this.getActionById(actionId);
6506
+ const action = this.getById(actionId);
6426
6507
  if (!action) {
6427
6508
  throw new Error(`Action "${actionId}" not found`);
6428
6509
  }
6429
- // Check if enabled
6430
- if (!runInInjectionContext(this.injector, () => action.enabled(context.message, context.currentUser))) {
6510
+ if (!this.isActionEnabled(action, context)) {
6431
6511
  throw new Error(`Action "${actionId}" is not enabled`);
6432
6512
  }
6433
- // Execute handler
6434
- await runInInjectionContext(this.injector, () => action.handler(context));
6435
- }
6436
- /**
6437
- * Clear all actions
6438
- */
6439
- clear() {
6440
- this._actions.set([]);
6513
+ if (action.handler) {
6514
+ await runInInjectionContext(this.injector, () => action.handler?.(context));
6515
+ }
6441
6516
  }
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 }); }
6517
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6518
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry }); }
6444
6519
  }
6445
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry, decorators: [{
6520
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationItemActionRegistry, decorators: [{
6446
6521
  type: Injectable
6447
6522
  }], ctorParameters: () => [] });
6448
6523
 
6449
6524
  /**
6450
- * IndexedDB Storage Wrapper
6451
- * Provides persistent storage for conversation data
6525
+ * Conversation Tab Registry
6526
+ * Register custom tabs for filtering conversations in the sidebar
6452
6527
  */
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 {
6528
+ /**
6529
+ * Conversation Tab Registry Service
6530
+ */
6531
+ class AXConversationTabRegistry {
6464
6532
  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;
6507
- }
6508
- /**
6509
- * Get a value from a store
6510
- */
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
- });
6523
- }
6524
- /**
6525
- * Get all values from a store
6526
- */
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
- });
6539
- }
6540
- /**
6541
- * Get values by index
6542
- */
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
- });
6556
- }
6557
- /**
6558
- * Put a value into a store
6559
- */
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
- });
6572
- }
6573
- /**
6574
- * Delete a value from a store
6575
- */
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
- });
6533
+ this._tabs = signal([], ...(ngDevMode ? [{ debugName: "_tabs" }] : []));
6534
+ this.translation = inject(AXTranslationService);
6535
+ /** All registered tabs */
6536
+ this.tabs = this._tabs.asReadonly();
6537
+ /** Enabled tabs sorted by priority */
6538
+ this.enabledTabs = computed(() => [...this._tabs()].filter((tab) => tab.enabled !== false).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "enabledTabs" }] : []));
6539
+ /** Default active tab */
6540
+ this.defaultTab = computed(() => this.enabledTabs().find((tab) => tab.default === true) || this.enabledTabs()[0], ...(ngDevMode ? [{ debugName: "defaultTab" }] : []));
6541
+ // Register default tabs from individual token
6542
+ inject(DEFAULT_CONVERSATION_TABS, { optional: true })?.forEach((tab) => this.register(tab));
6543
+ // Register tabs from REGISTRY_CONFIG
6544
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6545
+ config?.conversationTabs?.forEach((tab) => this.register(tab));
6588
6546
  }
6589
6547
  /**
6590
- * Clear all data from a store
6548
+ * Register a conversation tab
6591
6549
  */
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);
6550
+ register(tab) {
6551
+ // Validate tab
6552
+ if (!tab.id) {
6553
+ throw new Error('Conversation tab must have an id');
6554
+ }
6555
+ if (!tab.label) {
6556
+ throw new Error('Conversation tab must have a label');
6557
+ }
6558
+ if (!tab.filter) {
6559
+ throw new Error('Conversation tab must have a filter function');
6560
+ }
6561
+ // Check for duplicates
6562
+ const existing = this._tabs().find((t) => t.id === tab.id);
6563
+ if (existing) {
6564
+ console.warn(`Conversation tab "${tab.id}" is already registered. Replacing...`);
6565
+ this.unregister(tab.id);
6566
+ }
6567
+ // Add to registry
6568
+ this._tabs.update((tabs) => [...tabs, tab]);
6613
6569
  }
6614
- async putParticipant(participant) {
6615
- return this.put(AXConversationIndexedDbStores.PARTICIPANTS, participant);
6570
+ /**
6571
+ * Unregister a conversation tab
6572
+ */
6573
+ unregister(id) {
6574
+ this._tabs.update((tabs) => tabs.filter((t) => t.id !== id));
6616
6575
  }
6617
- async getConversation(id) {
6618
- return this.get(AXConversationIndexedDbStores.CONVERSATIONS, id);
6576
+ /**
6577
+ * Get tab by ID
6578
+ */
6579
+ getTabById(id) {
6580
+ return this._tabs().find((t) => t.id === id);
6619
6581
  }
6620
- async getAllConversations() {
6621
- return this.getAll(AXConversationIndexedDbStores.CONVERSATIONS);
6582
+ getTabLabel(tab) {
6583
+ return this.translation.translateSync(tab.label);
6622
6584
  }
6623
- async putConversation(conversation) {
6624
- return this.put(AXConversationIndexedDbStores.CONVERSATIONS, conversation);
6585
+ /**
6586
+ * Apply tab filter to conversations
6587
+ */
6588
+ filterConversations(tabId, conversations) {
6589
+ const tab = this.getTabById(tabId);
6590
+ if (!tab) {
6591
+ throw new Error(`Tab "${tabId}" not found`);
6592
+ }
6593
+ return tab.filter(conversations);
6625
6594
  }
6626
- async deleteConversation(id) {
6627
- return this.delete(AXConversationIndexedDbStores.CONVERSATIONS, id);
6595
+ /**
6596
+ * Get badge for a tab
6597
+ */
6598
+ getBadge(tabId, conversations) {
6599
+ const tab = this.getTabById(tabId);
6600
+ if (!tab || !tab.badge) {
6601
+ return undefined;
6602
+ }
6603
+ return tab.badge(conversations);
6628
6604
  }
6629
- async getMessage(id) {
6630
- return this.get(AXConversationIndexedDbStores.MESSAGES, id);
6605
+ /**
6606
+ * Enable/disable a tab
6607
+ */
6608
+ setEnabled(id, enabled) {
6609
+ this._tabs.update((tabs) => tabs.map((tab) => (tab.id === id ? { ...tab, enabled } : tab)));
6631
6610
  }
6632
- async getAllMessages() {
6633
- return this.getAll(AXConversationIndexedDbStores.MESSAGES);
6611
+ /**
6612
+ * Set default tab
6613
+ */
6614
+ setDefault(id) {
6615
+ this._tabs.update((tabs) => tabs.map((tab) => ({
6616
+ ...tab,
6617
+ default: tab.id === id,
6618
+ })));
6634
6619
  }
6635
- async getMessagesByConversation(conversationId) {
6636
- return this.getAllByIndex(AXConversationIndexedDbStores.MESSAGES, 'conversationId', conversationId);
6620
+ /**
6621
+ * Clear all tabs
6622
+ */
6623
+ clear() {
6624
+ this._tabs.set([]);
6637
6625
  }
6638
- async putMessage(message) {
6639
- return this.put(AXConversationIndexedDbStores.MESSAGES, message);
6626
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6627
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry }); }
6628
+ }
6629
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationTabRegistry, decorators: [{
6630
+ type: Injectable
6631
+ }], ctorParameters: () => [] });
6632
+
6633
+ /**
6634
+ * Info Bar Action Registry
6635
+ * Manages actions available in the conversation info bar
6636
+ */
6637
+ /**
6638
+ * Info Bar Action Registry Service
6639
+ * Manages available actions in the conversation info bar
6640
+ */
6641
+ class AXInfoBarActionRegistry extends AXBaseRegistry {
6642
+ constructor() {
6643
+ super();
6644
+ this.translation = inject(AXTranslationService);
6645
+ this.injector = inject(Injector);
6646
+ // Register default actions from individual token
6647
+ inject(DEFAULT_INFO_BAR_ACTIONS, { optional: true })?.forEach((action) => this.register(action));
6648
+ // Register actions from REGISTRY_CONFIG
6649
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6650
+ config?.infoBarActions?.forEach((action) => this.register(action));
6640
6651
  }
6641
- async deleteMessage(id) {
6642
- return this.delete(AXConversationIndexedDbStores.MESSAGES, id);
6652
+ /**
6653
+ * Get registry name for logging
6654
+ */
6655
+ getRegistryName() {
6656
+ return 'InfoBarActionRegistry';
6643
6657
  }
6644
- async getSetting(key) {
6645
- const result = await this.get(AXConversationIndexedDbStores.SETTINGS, key);
6646
- return result?.value;
6658
+ /**
6659
+ * Get actions for a specific conversation
6660
+ */
6661
+ getActionsForConversation(conversation, location) {
6662
+ const context = { conversation };
6663
+ return this.enabledItems()
6664
+ .filter((action) => {
6665
+ // Filter by location if specified
6666
+ if (location && action.location && action.location !== location) {
6667
+ return false;
6668
+ }
6669
+ // Filter by visibility
6670
+ if (action.visible && !runInInjectionContext(this.injector, () => action.visible?.(context))) {
6671
+ return false;
6672
+ }
6673
+ return true;
6674
+ })
6675
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
6647
6676
  }
6648
- async putSetting(key, value) {
6649
- return this.put(AXConversationIndexedDbStores.SETTINGS, { key, value });
6677
+ /**
6678
+ * Get inline actions (toolbar buttons)
6679
+ */
6680
+ getInlineActions(conversation) {
6681
+ return this.getActionsForConversation(conversation, 'inline');
6650
6682
  }
6651
- async getMedia(id) {
6652
- return this.get(AXConversationIndexedDbStores.MEDIA, id);
6683
+ /**
6684
+ * Get menu actions (dropdown items)
6685
+ */
6686
+ getMenuActions(conversation) {
6687
+ return this.getActionsForConversation(conversation, 'menu');
6653
6688
  }
6654
- async putMedia(record) {
6655
- return this.put(AXConversationIndexedDbStores.MEDIA, record);
6689
+ isActionEnabled(action, context) {
6690
+ if (action.enabled === false) {
6691
+ return false;
6692
+ }
6693
+ if (action.enabledWhen) {
6694
+ return runInInjectionContext(this.injector, () => action.enabledWhen?.(context)) !== false;
6695
+ }
6696
+ return true;
6656
6697
  }
6657
- async deleteMedia(id) {
6658
- return this.delete(AXConversationIndexedDbStores.MEDIA, id);
6698
+ /**
6699
+ * Get label for an action
6700
+ */
6701
+ getActionLabel(action, conversation) {
6702
+ if (typeof action.label === 'function') {
6703
+ const labelFactory = action.label;
6704
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => labelFactory({ conversation })));
6705
+ }
6706
+ return this.translation.translateSync(action.label);
6659
6707
  }
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
6708
  /**
6673
- * Normalize optional avatar/icon values so empty or whitespace-only strings
6674
- * are treated as missing data.
6709
+ * Get icon for an action
6675
6710
  */
6676
- static normalizeOptionalMediaValue(value) {
6677
- if (typeof value !== 'string') {
6711
+ getActionIcon(action, conversation) {
6712
+ if (!action.icon)
6678
6713
  return undefined;
6714
+ if (typeof action.icon === 'function') {
6715
+ const iconFactory = action.icon;
6716
+ return runInInjectionContext(this.injector, () => iconFactory({ conversation }));
6679
6717
  }
6680
- const normalized = value.trim();
6681
- return normalized.length > 0 ? normalized : undefined;
6718
+ return action.icon;
6682
6719
  }
6683
6720
  /**
6684
- * Get conversation avatar image URL.
6721
+ * Get tooltip for an action
6685
6722
  */
6686
- static getConversationAvatar(conversation) {
6687
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.avatar);
6723
+ getActionTooltip(action, conversation) {
6724
+ if (!action.tooltip)
6725
+ return undefined;
6726
+ if (typeof action.tooltip === 'function') {
6727
+ const tooltipFactory = action.tooltip;
6728
+ return this.translation.translateSync(runInInjectionContext(this.injector, () => tooltipFactory({ conversation })));
6729
+ }
6730
+ return this.translation.translateSync(action.tooltip);
6731
+ }
6732
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6733
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry }); }
6734
+ }
6735
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXInfoBarActionRegistry, decorators: [{
6736
+ type: Injectable
6737
+ }], ctorParameters: () => [] });
6738
+
6739
+ /**
6740
+ * Message Action Registry
6741
+ * Register custom actions that can be performed on messages
6742
+ */
6743
+ /**
6744
+ * Message Action Registry Service
6745
+ */
6746
+ class AXMessageActionRegistry {
6747
+ constructor() {
6748
+ this._actions = signal([], ...(ngDevMode ? [{ debugName: "_actions" }] : []));
6749
+ this.translation = inject(AXTranslationService);
6750
+ this.injector = inject(Injector);
6751
+ /** All registered actions */
6752
+ this.actions = this._actions.asReadonly();
6753
+ /** Actions sorted by priority */
6754
+ this.sortedActions = computed(() => [...this._actions()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)), ...(ngDevMode ? [{ debugName: "sortedActions" }] : []));
6755
+ // Register default actions from individual token
6756
+ const defaultActions = inject(DEFAULT_MESSAGE_ACTIONS, { optional: true });
6757
+ if (defaultActions) {
6758
+ defaultActions.forEach((action) => this.register(action));
6759
+ }
6760
+ // Register actions from REGISTRY_CONFIG
6761
+ const config = inject(REGISTRY_CONFIG, { optional: true });
6762
+ config?.messageActions?.forEach((action) => this.register(action));
6688
6763
  }
6689
6764
  /**
6690
- * Font Awesome icon class(es) for a conversation when there is no avatar image.
6765
+ * Register a message action
6691
6766
  */
6692
- static getConversationAvatarIcon(conversation) {
6693
- if (AXConversationMessageUtilsService.getConversationAvatar(conversation)) {
6694
- return undefined;
6767
+ register(action) {
6768
+ // Validate action
6769
+ if (!action.id) {
6770
+ throw new Error('Message action must have an id');
6695
6771
  }
6696
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.icon);
6772
+ if (!action.label) {
6773
+ throw new Error('Message action must have a label');
6774
+ }
6775
+ if (!action.handler) {
6776
+ throw new Error('Message action must have a handler');
6777
+ }
6778
+ // Check for duplicates
6779
+ const existing = this._actions().find((a) => a.id === action.id);
6780
+ if (existing) {
6781
+ console.warn(`Message action "${action.id}" is already registered. Replacing...`);
6782
+ this.unregister(action.id);
6783
+ }
6784
+ // Add to registry
6785
+ this._actions.update((actions) => [...actions, action]);
6697
6786
  }
6698
6787
  /**
6699
- * Get sender name from message
6788
+ * Unregister a message action
6700
6789
  */
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');
6790
+ unregister(id) {
6791
+ this._actions.update((actions) => actions.filter((a) => a.id !== id));
6704
6792
  }
6705
6793
  /**
6706
- * Get sender avatar image URL (takes precedence over {@link getSenderAvatarIcon}).
6794
+ * Get all actions for a specific message
6707
6795
  */
6708
- static getSenderAvatar(message, conversation) {
6709
- const participant = conversation.participants.find((p) => p.id === message.senderId);
6710
- return AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar);
6796
+ getActions(message, currentUser) {
6797
+ return this.sortedActions().filter((action) => runInInjectionContext(this.injector, () => action.visible(message, currentUser)) &&
6798
+ runInInjectionContext(this.injector, () => action.enabled(message, currentUser)));
6711
6799
  }
6712
6800
  /**
6713
- * Font Awesome icon class(es) for the sender when there is no avatar image:
6714
- * participant `icon` first, then conversation-level `icon`.
6801
+ * Get menu actions for a message
6715
6802
  */
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));
6803
+ getMenuActions(message, currentUser) {
6804
+ return this.getActions(message, currentUser);
6805
+ }
6806
+ /** Whether an action is enabled (safe to call outside an injection context). */
6807
+ isActionEnabled(action, message, currentUser) {
6808
+ return runInInjectionContext(this.injector, () => action.enabled(message, currentUser));
6809
+ }
6810
+ getActionLabel(action) {
6811
+ return this.translation.translateSync(action.label);
6723
6812
  }
6724
6813
  /**
6725
- * Get initials from name
6814
+ * Get inline actions for a message
6726
6815
  */
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);
6816
+ getInlineActions(message, currentUser) {
6817
+ return this.getActions(message, currentUser).filter((action) => action.showInline === true);
6736
6818
  }
6737
6819
  /**
6738
- * Type guard for text payload
6820
+ * Get action by ID
6739
6821
  */
6740
- static isTextPayload(payload) {
6741
- return 'text' in payload && typeof payload.text === 'string';
6822
+ getActionById(id) {
6823
+ return this._actions().find((a) => a.id === id);
6742
6824
  }
6743
6825
  /**
6744
- * Get message text content
6826
+ * Execute an action
6745
6827
  */
6746
- static getMessageText(message) {
6747
- if (message.type === 'text' && AXConversationMessageUtilsService.isTextPayload(message.payload)) {
6748
- return message.payload.text;
6828
+ async executeAction(actionId, context) {
6829
+ const action = this.getActionById(actionId);
6830
+ if (!action) {
6831
+ throw new Error(`Action "${actionId}" not found`);
6749
6832
  }
6750
- return `[${message.type}]`;
6833
+ // Check if enabled
6834
+ if (!runInInjectionContext(this.injector, () => action.enabled(context.message, context.currentUser))) {
6835
+ throw new Error(`Action "${actionId}" is not enabled`);
6836
+ }
6837
+ // Execute handler
6838
+ await runInInjectionContext(this.injector, () => action.handler(context));
6751
6839
  }
6752
6840
  /**
6753
- * Format message preview text
6841
+ * Clear all actions
6754
6842
  */
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
- };
6847
- }
6843
+ clear() {
6844
+ this._actions.set([]);
6845
+ }
6846
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6847
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry }); }
6848
+ }
6849
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXMessageActionRegistry, decorators: [{
6850
+ type: Injectable
6851
+ }], ctorParameters: () => [] });
6852
+
6853
+ /**
6854
+ * IndexedDB Storage Wrapper
6855
+ * Provides persistent storage for conversation data
6856
+ */
6857
+ const DB_NAME = 'acorex-conversation-db';
6858
+ const DB_VERSION = 2;
6859
+ // Store names
6860
+ const AXConversationIndexedDbStores = {
6861
+ PARTICIPANTS: 'participants',
6862
+ CONVERSATIONS: 'conversations',
6863
+ MESSAGES: 'messages',
6864
+ MEDIA: 'media',
6865
+ SETTINGS: 'settings',
6866
+ };
6867
+ class AXConversationIndexedDbStorage {
6868
+ constructor() {
6869
+ this.db = null;
6870
+ this.initPromise = null;
6848
6871
  }
6849
6872
  /**
6850
- * Check if message is from current user
6873
+ * Initialize IndexedDB
6851
6874
  */
6852
- static isOwnMessage(message, currentUserId) {
6853
- return message.senderId === currentUserId;
6875
+ async init() {
6876
+ if (this.db)
6877
+ return;
6878
+ if (this.initPromise)
6879
+ return this.initPromise;
6880
+ this.initPromise = new Promise((resolve, reject) => {
6881
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
6882
+ request.onerror = () => reject(request.error);
6883
+ request.onsuccess = () => {
6884
+ this.db = request.result;
6885
+ resolve();
6886
+ };
6887
+ request.onupgradeneeded = (event) => {
6888
+ const db = event.target.result;
6889
+ // Create object stores if they don't exist
6890
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.PARTICIPANTS)) {
6891
+ db.createObjectStore(AXConversationIndexedDbStores.PARTICIPANTS, { keyPath: 'id' });
6892
+ }
6893
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.CONVERSATIONS)) {
6894
+ const convStore = db.createObjectStore(AXConversationIndexedDbStores.CONVERSATIONS, { keyPath: 'id' });
6895
+ convStore.createIndex('lastMessageAt', 'lastMessageAt', { unique: false });
6896
+ }
6897
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MESSAGES)) {
6898
+ const msgStore = db.createObjectStore(AXConversationIndexedDbStores.MESSAGES, { keyPath: 'id' });
6899
+ msgStore.createIndex('conversationId', 'conversationId', { unique: false });
6900
+ msgStore.createIndex('timestamp', 'timestamp', { unique: false });
6901
+ }
6902
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.SETTINGS)) {
6903
+ db.createObjectStore(AXConversationIndexedDbStores.SETTINGS, { keyPath: 'key' });
6904
+ }
6905
+ if (!db.objectStoreNames.contains(AXConversationIndexedDbStores.MEDIA)) {
6906
+ db.createObjectStore(AXConversationIndexedDbStores.MEDIA, { keyPath: 'id' });
6907
+ }
6908
+ };
6909
+ });
6910
+ return this.initPromise;
6854
6911
  }
6855
6912
  /**
6856
- * Get message status icon class (Font Awesome)
6913
+ * Get a value from a store
6857
6914
  */
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
- }
6915
+ async get(storeName, key) {
6916
+ await this.init();
6917
+ if (!this.db)
6918
+ throw new Error('Database not initialized');
6919
+ const db = this.db;
6920
+ return new Promise((resolve, reject) => {
6921
+ const transaction = db.transaction(storeName, 'readonly');
6922
+ const store = transaction.objectStore(storeName);
6923
+ const request = store.get(key);
6924
+ request.onsuccess = () => resolve(request.result);
6925
+ request.onerror = () => reject(request.error);
6926
+ });
6873
6927
  }
6874
6928
  /**
6875
- * Check if message should show avatar
6929
+ * Get all values from a store
6876
6930
  */
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;
6931
+ async getAll(storeName) {
6932
+ await this.init();
6933
+ if (!this.db)
6934
+ throw new Error('Database not initialized');
6935
+ const db = this.db;
6936
+ return new Promise((resolve, reject) => {
6937
+ const transaction = db.transaction(storeName, 'readonly');
6938
+ const store = transaction.objectStore(storeName);
6939
+ const request = store.getAll();
6940
+ request.onsuccess = () => resolve(request.result);
6941
+ request.onerror = () => reject(request.error);
6942
+ });
6888
6943
  }
6889
6944
  /**
6890
- * Group messages by sender for consecutive messages
6945
+ * Get values by index
6891
6946
  */
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;
6947
+ async getAllByIndex(storeName, indexName, query) {
6948
+ await this.init();
6949
+ if (!this.db)
6950
+ throw new Error('Database not initialized');
6951
+ const db = this.db;
6952
+ return new Promise((resolve, reject) => {
6953
+ const transaction = db.transaction(storeName, 'readonly');
6954
+ const store = transaction.objectStore(storeName);
6955
+ const index = store.index(indexName);
6956
+ const request = index.getAll(query);
6957
+ request.onsuccess = () => resolve(request.result);
6958
+ request.onerror = () => reject(request.error);
6959
+ });
6901
6960
  }
6902
6961
  /**
6903
- * Get conversation status for avatar (private conversations only)
6962
+ * Put a value into a store
6904
6963
  */
6905
- static getConversationStatus(conversation) {
6906
- if (conversation.type === 'private') {
6907
- return conversation.status.presence;
6908
- }
6909
- return undefined;
6964
+ async put(storeName, value) {
6965
+ await this.init();
6966
+ if (!this.db)
6967
+ throw new Error('Database not initialized');
6968
+ const db = this.db;
6969
+ return new Promise((resolve, reject) => {
6970
+ const transaction = db.transaction(storeName, 'readwrite');
6971
+ const store = transaction.objectStore(storeName);
6972
+ const request = store.put(value);
6973
+ request.onsuccess = () => resolve();
6974
+ request.onerror = () => reject(request.error);
6975
+ });
6910
6976
  }
6911
6977
  /**
6912
- * Get typing indicator text for conversation
6978
+ * Delete a value from a store
6913
6979
  */
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 } });
6980
+ async delete(storeName, key) {
6981
+ await this.init();
6982
+ if (!this.db)
6983
+ throw new Error('Database not initialized');
6984
+ const db = this.db;
6985
+ return new Promise((resolve, reject) => {
6986
+ const transaction = db.transaction(storeName, 'readwrite');
6987
+ const store = transaction.objectStore(storeName);
6988
+ const request = store.delete(key);
6989
+ request.onsuccess = () => resolve();
6990
+ request.onerror = () => reject(request.error);
6991
+ });
6928
6992
  }
6929
6993
  /**
6930
- * Format last seen time
6994
+ * Clear all data from a store
6931
6995
  */
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();
6996
+ async clear(storeName) {
6997
+ await this.init();
6998
+ if (!this.db)
6999
+ throw new Error('Database not initialized');
7000
+ const db = this.db;
7001
+ return new Promise((resolve, reject) => {
7002
+ const transaction = db.transaction(storeName, 'readwrite');
7003
+ const store = transaction.objectStore(storeName);
7004
+ const request = store.clear();
7005
+ request.onsuccess = () => resolve();
7006
+ request.onerror = () => reject(request.error);
7007
+ });
6945
7008
  }
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
- }
7009
+ // =====================
7010
+ // Convenience Methods
7011
+ // =====================
7012
+ async getParticipant(id) {
7013
+ return this.get(AXConversationIndexedDbStores.PARTICIPANTS, id);
7014
+ }
7015
+ async getAllParticipants() {
7016
+ return this.getAll(AXConversationIndexedDbStores.PARTICIPANTS);
7017
+ }
7018
+ async putParticipant(participant) {
7019
+ return this.put(AXConversationIndexedDbStores.PARTICIPANTS, participant);
7020
+ }
7021
+ async getConversation(id) {
7022
+ return this.get(AXConversationIndexedDbStores.CONVERSATIONS, id);
7023
+ }
7024
+ async getAllConversations() {
7025
+ return this.getAll(AXConversationIndexedDbStores.CONVERSATIONS);
7026
+ }
7027
+ async putConversation(conversation) {
7028
+ return this.put(AXConversationIndexedDbStores.CONVERSATIONS, conversation);
7029
+ }
7030
+ async deleteConversation(id) {
7031
+ return this.delete(AXConversationIndexedDbStores.CONVERSATIONS, id);
7032
+ }
7033
+ async getMessage(id) {
7034
+ return this.get(AXConversationIndexedDbStores.MESSAGES, id);
7035
+ }
7036
+ async getAllMessages() {
7037
+ return this.getAll(AXConversationIndexedDbStores.MESSAGES);
7038
+ }
7039
+ async getMessagesByConversation(conversationId) {
7040
+ return this.getAllByIndex(AXConversationIndexedDbStores.MESSAGES, 'conversationId', conversationId);
7041
+ }
7042
+ async putMessage(message) {
7043
+ return this.put(AXConversationIndexedDbStores.MESSAGES, message);
7044
+ }
7045
+ async deleteMessage(id) {
7046
+ return this.delete(AXConversationIndexedDbStores.MESSAGES, id);
7047
+ }
7048
+ async getSetting(key) {
7049
+ const result = await this.get(AXConversationIndexedDbStores.SETTINGS, key);
7050
+ return result?.value;
7051
+ }
7052
+ async putSetting(key, value) {
7053
+ return this.put(AXConversationIndexedDbStores.SETTINGS, { key, value });
7054
+ }
7055
+ async getMedia(id) {
7056
+ return this.get(AXConversationIndexedDbStores.MEDIA, id);
7057
+ }
7058
+ async putMedia(record) {
7059
+ return this.put(AXConversationIndexedDbStores.MEDIA, record);
7060
+ }
7061
+ async deleteMedia(id) {
7062
+ return this.delete(AXConversationIndexedDbStores.MEDIA, id);
6973
7063
  }
6974
7064
  }
7065
+ // Export singleton instance
7066
+ const axConversationIndexedDbStorage = new AXConversationIndexedDbStorage();
7067
+
7068
+ var indexeddbStorage = /*#__PURE__*/Object.freeze({
7069
+ __proto__: null,
7070
+ AXConversationIndexedDbStorage: AXConversationIndexedDbStorage,
7071
+ AXConversationIndexedDbStores: AXConversationIndexedDbStores,
7072
+ axConversationIndexedDbStorage: axConversationIndexedDbStorage
7073
+ });
6975
7074
 
6976
7075
  /**
6977
7076
  * Shared read/write helpers for IndexedDB chat storage (in-memory + persistence).
@@ -8417,20 +8516,27 @@ class AXConversationIndexedDbConversationApi extends AXConversationApi {
8417
8516
  // Conversation CRUD
8418
8517
  // =====================
8419
8518
  async createConversation(data) {
8519
+ const creatorId = data.creatorId ?? conversationSharedStorage.currentUserId;
8520
+ const participantIds = new Set(data.participantIds);
8521
+ if (creatorId) {
8522
+ participantIds.add(creatorId);
8523
+ }
8420
8524
  const conversation = {
8421
8525
  id: `conv-${Date.now()}`,
8422
8526
  type: data.type,
8423
- title: data.title || (data.type === 'private' ? 'New Chat' : data.type === 'group' ? 'New Group' : 'New Channel'),
8527
+ creatorId,
8528
+ title: data.title ||
8529
+ (data.type === 'private' ? 'New Chat' : data.type === 'group' ? 'New Group' : 'New Channel'),
8424
8530
  avatar: data.avatar,
8425
8531
  description: data.description,
8426
- participants: data.participantIds.map((id) => {
8532
+ participants: [...participantIds].map((id) => {
8427
8533
  const participant = conversationSharedStorage.participants.get(id);
8428
8534
  if (!participant) {
8429
8535
  throw this.createError('PARTICIPANT_NOT_FOUND', `Participant ${id} not found`, 404);
8430
8536
  }
8431
8537
  return {
8432
8538
  ...participant,
8433
- role: id === conversationSharedStorage.currentUserId ? 'admin' : 'member',
8539
+ role: id === creatorId ? 'admin' : 'member',
8434
8540
  };
8435
8541
  }),
8436
8542
  lastMessageAt: new Date(),
@@ -10400,41 +10506,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
10400
10506
  `, 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
10507
  }] });
10402
10508
 
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
10509
  /**
10439
10510
  * Conversation avatar host — renders a registered custom component or built-in fallback.
10440
10511
  */
@@ -10519,7 +10590,7 @@ class AXConversationAvatarComponent {
10519
10590
  if (!conversation) {
10520
10591
  return { name: explicitName ?? '?', avatar: explicitAvatar, icon: explicitIcon };
10521
10592
  }
10522
- const resolved = resolveConversationAvatarDisplay(conversation);
10593
+ const resolved = resolveConversationAvatarDisplay(conversation, this.currentUserId());
10523
10594
  return {
10524
10595
  name: explicitName ?? resolved.name,
10525
10596
  avatar: explicitAvatar ?? resolved.avatar,
@@ -14923,14 +14994,23 @@ class AXConversationService {
14923
14994
  this._messageDeleted$ = new Subject();
14924
14995
  this._typingIndicator$ = new Subject();
14925
14996
  this._presenceUpdate$ = new Subject();
14926
- /** All conversations */
14927
- this.conversations = this.state.conversations;
14997
+ /** All conversations (viewer-scoped display titles for private 1v1 chats) */
14998
+ this.conversations = computed(() => {
14999
+ const currentUserId = this._currentUser()?.id;
15000
+ return this.state.conversations().map((conversation) => resolveConversationForViewer(conversation, currentUserId));
15001
+ }, ...(ngDevMode ? [{ debugName: "conversations" }] : []));
14928
15002
  /** Active conversation ID */
14929
15003
  this.activeConversationId = this._activeConversationId.asReadonly();
14930
15004
  /** Active conversation */
14931
15005
  this.activeConversation = computed(() => {
14932
15006
  const id = this._activeConversationId();
14933
- return id ? this.state.getConversation(id) : null;
15007
+ if (!id) {
15008
+ return null;
15009
+ }
15010
+ const conversation = this.state.getConversation(id);
15011
+ return conversation
15012
+ ? resolveConversationForViewer(conversation, this._currentUser()?.id)
15013
+ : null;
14934
15014
  }, ...(ngDevMode ? [{ debugName: "activeConversation" }] : []));
14935
15015
  /** Messages for active conversation */
14936
15016
  this.activeMessages = computed(() => {
@@ -15721,17 +15801,29 @@ class AXConversationService {
15721
15801
  */
15722
15802
  async createConversation(participantIds, type, metadata) {
15723
15803
  try {
15804
+ const currentUser = this._currentUser();
15805
+ const creatorId = currentUser?.id;
15806
+ const isPrivate = type === 'private';
15807
+ const scopedParticipantIds = [...participantIds];
15808
+ if (creatorId && !scopedParticipantIds.includes(creatorId)) {
15809
+ scopedParticipantIds.unshift(creatorId);
15810
+ }
15724
15811
  const conversation = await this.conversationApi.createConversation({
15725
15812
  type,
15726
- participantIds,
15727
- title: metadata?.['title'],
15813
+ participantIds: scopedParticipantIds,
15814
+ creatorId,
15815
+ title: isPrivate ? undefined : metadata?.['title'],
15728
15816
  description: metadata?.['description'],
15729
- avatar: AXConversationService.normalizeOptionalString(metadata?.['avatar']),
15730
- icon: AXConversationService.normalizeOptionalString(metadata?.['icon']),
15731
- metadata,
15817
+ avatar: isPrivate
15818
+ ? undefined
15819
+ : AXConversationService.normalizeOptionalString(metadata?.['avatar']),
15820
+ icon: isPrivate
15821
+ ? undefined
15822
+ : AXConversationService.normalizeOptionalString(metadata?.['icon']),
15823
+ metadata: isPrivate ? undefined : metadata,
15732
15824
  });
15733
15825
  this.state.setConversation(conversation);
15734
- return conversation;
15826
+ return resolveConversationForViewer(conversation, creatorId);
15735
15827
  }
15736
15828
  catch (error) {
15737
15829
  this.errorHandler.handle(error, 'createConversation', { participantIds, type });
@@ -20960,12 +21052,7 @@ class AXNewConversationDialogComponent extends AXBasePageComponent {
20960
21052
  try {
20961
21053
  let conversation;
20962
21054
  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);
21055
+ conversation = await this.conversationService.createConversation(selectedIds, 'private');
20969
21056
  }
20970
21057
  else {
20971
21058
  const metadata = {
@@ -22179,5 +22266,5 @@ function getErrorMessage(code, params) {
22179
22266
  * Generated bundle index. Do not edit.
22180
22267
  */
22181
22268
 
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 };
22269
+ 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, isGenericPrivateConversationTitle, mergeUploadResult, mergeWithDefaults, normalizeAllConversationMessageIndexes, paginateChatNewestFirst, paginateChatOldestFirst, parseChatCursor, provideConversation, provideConversationComposerFileTypes, provideConversationFileCatalog, registerChatMessage, resolveConversationAvatarDisplay, resolveConversationForViewer, resolveConversationTitleForViewer, resolveParticipantProfile, resolvePrivatePeerParticipant, resolvePrivatePeerUserId, resolveUserAvatarDisplay, revokeObjectUrl, sanitizeInput, shouldUseUserAvatarForConversation, sortConversationMessageIds, toUploaderReference$1 as toUploaderReference, unregisterChatMessage, validateConversationId, validateEmail, validateLatitude, validateLongitude, validateMessagePayload, validateMessageText, validateMessageType, validateUrl, validateUserId, validateUserIds };
22183
22270
  //# sourceMappingURL=acorex-components-conversation2.mjs.map