@micha.bigler/ui-core-micha 2.14.0 → 2.16.0

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.
Files changed (71) hide show
  1. package/README.md +7 -0
  2. package/WORK_ORDERS.md +8 -2
  3. package/dev/entries.jsx +147 -0
  4. package/dev/fixtures.js +26 -0
  5. package/dev/main.jsx +56 -0
  6. package/dev/mockTransport.jsx +128 -0
  7. package/dist/components/charts/BarChart.js +32 -0
  8. package/dist/components/charts/ChartFrame.js +50 -0
  9. package/dist/components/charts/LineChart.js +32 -0
  10. package/dist/components/charts/exportChart.js +67 -0
  11. package/dist/components/charts/formatters.js +23 -0
  12. package/dist/components/charts/palette.js +21 -0
  13. package/dist/i18n/chartsTranslations.js +38 -0
  14. package/dist/i18n/messagingTranslations.js +81 -0
  15. package/dist/index.js +25 -4
  16. package/dist/messaging/AttachmentList.js +43 -0
  17. package/dist/messaging/Composer.js +120 -0
  18. package/dist/messaging/ConversationLaunchers.js +39 -0
  19. package/dist/messaging/ConversationList.js +75 -0
  20. package/dist/messaging/MessageBubble.js +17 -0
  21. package/dist/messaging/MessagingProvider.js +317 -0
  22. package/dist/messaging/MessagingScopeConfig.js +28 -0
  23. package/dist/messaging/PollCard.js +53 -0
  24. package/dist/messaging/ReactionBar.js +44 -0
  25. package/dist/messaging/ReadTicks.js +25 -0
  26. package/dist/messaging/Thread.js +73 -0
  27. package/dist/messaging/api.js +51 -0
  28. package/dist/notifications/NotificationsProvider.js +2 -2
  29. package/dist/notifications/realtime.js +21 -1
  30. package/docs/messaging-deviations.md +36 -0
  31. package/index.html +12 -0
  32. package/package.json +6 -1
  33. package/src/components/charts/BarChart.jsx +44 -0
  34. package/src/components/charts/ChartFrame.jsx +112 -0
  35. package/src/components/charts/LineChart.jsx +44 -0
  36. package/src/components/charts/exportChart.js +71 -0
  37. package/src/components/charts/formatters.js +39 -0
  38. package/src/components/charts/palette.js +24 -0
  39. package/src/i18n/chartsTranslations.ts +38 -0
  40. package/src/i18n/messagingTranslations.ts +81 -0
  41. package/src/index.js +33 -4
  42. package/src/messaging/AttachmentList.jsx +38 -0
  43. package/src/messaging/Composer.jsx +110 -0
  44. package/src/messaging/ConversationLaunchers.jsx +51 -0
  45. package/src/messaging/ConversationList.jsx +78 -0
  46. package/src/messaging/MessageBubble.jsx +37 -0
  47. package/src/messaging/MessagingProvider.jsx +323 -0
  48. package/src/messaging/MessagingScopeConfig.jsx +24 -0
  49. package/src/messaging/PollCard.jsx +36 -0
  50. package/src/messaging/ReactionBar.jsx +46 -0
  51. package/src/messaging/ReadTicks.jsx +23 -0
  52. package/src/messaging/Thread.jsx +71 -0
  53. package/src/messaging/api.js +47 -0
  54. package/src/notifications/NotificationsProvider.jsx +2 -2
  55. package/src/notifications/realtime.jsx +21 -1
  56. package/tests/BarChart.test.jsx +55 -0
  57. package/tests/ChartFrame.test.jsx +122 -0
  58. package/tests/ConversationList.test.jsx +66 -0
  59. package/tests/LineChart.test.jsx +33 -0
  60. package/tests/MessagingProvider.test.jsx +50 -0
  61. package/tests/Thread.test.jsx +100 -0
  62. package/tests/chartsPalette.test.js +39 -0
  63. package/tests/messagingApi.test.js +32 -0
  64. package/tests/messagingComposer.test.jsx +111 -0
  65. package/tests/messagingExports.test.js +20 -0
  66. package/tests/messagingInteractions.test.jsx +112 -0
  67. package/tests/realtime.test.jsx +27 -0
  68. package/vite.config.mjs +6 -0
  69. package/work-orders/CHART-1.md +285 -0
  70. package/work-orders/DX-1.md +166 -0
  71. package/work-orders/MSG-3.md +375 -0
@@ -0,0 +1,23 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ export function formatPercentage(value, locale, options = {}) {
3
+ return new Intl.NumberFormat(locale, Object.assign({ style: 'percent', maximumFractionDigits: 1 }, options)).format(value);
4
+ }
5
+ export function formatCompact(value, locale, options = {}) {
6
+ return new Intl.NumberFormat(locale, Object.assign({ notation: 'compact', maximumFractionDigits: 1 }, options)).format(value);
7
+ }
8
+ export function formatRatio(value, locale, options = {}) {
9
+ return new Intl.NumberFormat(locale, Object.assign({ maximumFractionDigits: 2 }, options)).format(value);
10
+ }
11
+ /** Creates locale-aware formatter functions for use in chart axes and tooltips. */
12
+ export function createChartFormatters(locale) {
13
+ return {
14
+ percentage: (value, options) => formatPercentage(value, locale, options),
15
+ compact: (value, options) => formatCompact(value, locale, options),
16
+ ratio: (value, options) => formatRatio(value, locale, options),
17
+ };
18
+ }
19
+ /** Resolves formatter locale from the active react-i18next instance. */
20
+ export function useChartFormatters() {
21
+ const { i18n } = useTranslation();
22
+ return createChartFormatters(i18n.language);
23
+ }
@@ -0,0 +1,21 @@
1
+ import { useTheme } from '@mui/material/styles';
2
+ /**
3
+ * Returns neutral chart colours derived only from the active MUI theme.
4
+ * Consumers may replace either array with their own domain palette.
5
+ */
6
+ export function getNeutralChartPalette(theme) {
7
+ const palette = theme.palette;
8
+ const isDark = palette.mode === 'dark';
9
+ return {
10
+ categorical: isDark
11
+ ? [palette.primary.light, palette.secondary.light, palette.info.light, palette.success.light, palette.warning.light]
12
+ : [palette.primary.main, palette.secondary.main, palette.info.main, palette.success.main, palette.warning.main],
13
+ sequential: isDark
14
+ ? [palette.action.disabled, palette.text.secondary, palette.primary.light, palette.primary.main, palette.primary.dark]
15
+ : [palette.action.disabled, palette.divider, palette.text.secondary, palette.primary.light, palette.primary.main],
16
+ };
17
+ }
18
+ /** Resolves the kit's neutral palette from the nearest MUI ThemeProvider. */
19
+ export function useNeutralChartPalette() {
20
+ return getNeutralChartPalette(useTheme());
21
+ }
@@ -0,0 +1,38 @@
1
+ export const chartsTranslations = {
2
+ 'ChartFrame.LOADING': {
3
+ de: 'Diagramm wird geladen.',
4
+ fr: 'Chargement du graphique.',
5
+ en: 'Loading chart.',
6
+ sw: 'Inapakia chati.',
7
+ },
8
+ 'ChartFrame.EMPTY_DEFAULT': {
9
+ de: 'Keine Daten verfügbar.',
10
+ fr: 'Aucune donnée disponible.',
11
+ en: 'No data available.',
12
+ sw: 'Hakuna data inayopatikana.',
13
+ },
14
+ 'ChartFrame.ERROR_DEFAULT': {
15
+ de: 'Das Diagramm konnte nicht geladen werden.',
16
+ fr: 'Impossible de charger le graphique.',
17
+ en: 'The chart could not be loaded.',
18
+ sw: 'Chati haikuweza kupakiwa.',
19
+ },
20
+ 'ChartFrame.EXPORT_SVG_LABEL': {
21
+ de: 'SVG exportieren',
22
+ fr: 'Exporter en SVG',
23
+ en: 'Export SVG',
24
+ sw: 'Hamisha SVG',
25
+ },
26
+ 'ChartFrame.EXPORT_PNG_LABEL': {
27
+ de: 'PNG exportieren',
28
+ fr: 'Exporter en PNG',
29
+ en: 'Export PNG',
30
+ sw: 'Hamisha PNG',
31
+ },
32
+ 'ChartFrame.EXPORT_ERROR': {
33
+ de: 'Das Diagramm konnte nicht exportiert werden.',
34
+ fr: 'Impossible d’exporter le graphique.',
35
+ en: 'The chart could not be exported.',
36
+ sw: 'Chati haikuweza kuhamishwa.',
37
+ },
38
+ };
@@ -0,0 +1,81 @@
1
+ export const messagingTranslations = {
2
+ 'MessagingList.LABEL': { de: 'Unterhaltungen', en: 'Conversations', fr: 'Conversations' },
3
+ 'MessagingList.EMPTY': { de: 'Noch keine Unterhaltungen.', en: 'No conversations yet.', fr: 'Aucune conversation pour le moment.' },
4
+ 'MessagingList.NO_MESSAGES': { de: 'Noch keine Nachrichten', en: 'No messages yet', fr: 'Aucun message pour le moment' },
5
+ 'MessagingList.UNTITLED': { de: 'Unbenannte Unterhaltung', en: 'Untitled conversation', fr: 'Conversation sans titre' },
6
+ 'MessagingList.ACTIONS': { de: 'Aktionen für Unterhaltung', en: 'Conversation actions', fr: 'Actions de la conversation' },
7
+ 'MessagingList.LOAD_MORE': { de: 'Mehr laden', en: 'Load more', fr: 'Charger plus' },
8
+ 'MessagingList.LOAD_ERROR': { de: 'Unterhaltungen konnten nicht geladen werden.', en: 'Conversations could not be loaded.', fr: 'Impossible de charger les conversations.' },
9
+ 'MessagingList.ARCHIVE': { de: 'Archivieren', en: 'Archive', fr: 'Archiver' },
10
+ 'MessagingList.ARCHIVE_ERROR': { de: 'Unterhaltung konnte nicht archiviert werden.', en: 'Conversation could not be archived.', fr: 'Impossible d’archiver la conversation.' },
11
+ 'MessagingList.MUTE': { de: 'Stummschalten', en: 'Mute', fr: 'Mettre en sourdine' },
12
+ 'MessagingList.UNMUTE': { de: 'Stummschaltung aufheben', en: 'Unmute', fr: 'Réactiver le son' },
13
+ 'MessagingList.PREFERENCES_ERROR': { de: 'Einstellungen konnten nicht gespeichert werden.', en: 'Preferences could not be saved.', fr: 'Impossible d’enregistrer les préférences.' },
14
+ 'MessagingLaunchers.OPEN_GROUP': { de: 'Gruppenunterhaltung öffnen', en: 'Open group conversation', fr: 'Ouvrir la conversation de groupe' },
15
+ 'MessagingLaunchers.OPEN_BROADCAST': { de: 'Ankündigungen öffnen', en: 'Open announcements', fr: 'Ouvrir les annonces' },
16
+ 'MessagingLaunchers.OPEN_ERROR': { de: 'Unterhaltung konnte nicht geöffnet werden.', en: 'Conversation could not be opened.', fr: 'Impossible d’ouvrir la conversation.' },
17
+ 'MessagingThread.TIMELINE': { de: 'Nachrichtenverlauf', en: 'Message timeline', fr: 'Chronologie des messages' },
18
+ 'MessagingThread.MESSAGE': { de: 'Nachricht', en: 'Message', fr: 'Message' },
19
+ 'MessagingThread.EMPTY': { de: 'Noch keine Nachrichten.', en: 'No messages yet.', fr: 'Aucun message pour le moment.' },
20
+ 'MessagingThread.LOAD_OLDER': { de: 'Ältere Nachrichten laden', en: 'Load older messages', fr: 'Charger les messages plus anciens' },
21
+ 'MessagingThread.LOAD_ERROR': { de: 'Nachrichten konnten nicht geladen werden.', en: 'Messages could not be loaded.', fr: 'Impossible de charger les messages.' },
22
+ 'MessagingThread.REPLIES_ERROR': { de: 'Antworten konnten nicht geladen werden.', en: 'Replies could not be loaded.', fr: 'Impossible de charger les réponses.' },
23
+ 'MessagingThread.DELETED': { de: 'Diese Nachricht wurde gelöscht.', en: 'This message was deleted.', fr: 'Ce message a été supprimé.' },
24
+ 'MessagingThread.EDITED': { de: 'bearbeitet', en: 'edited', fr: 'modifié' },
25
+ 'MessagingThread.REPLY': { de: 'Antworten', en: 'Reply', fr: 'Répondre' },
26
+ 'MessagingThread.REPLY_TO': { de: 'Antwort auf {{sender}}', en: 'Reply to {{sender}}', fr: 'Réponse à {{sender}}' },
27
+ 'MessagingThread.REPLYING_TO': { de: 'Antwort an {{sender}}', en: 'Replying to {{sender}}', fr: 'Réponse à {{sender}}' },
28
+ 'MessagingThread.SHOW_REPLIES': { de: '{{count}} Antworten anzeigen', en: 'Show {{count}} replies', fr: 'Afficher {{count}} réponses' },
29
+ 'MessagingThread.HIDE_REPLIES': { de: 'Antworten ausblenden', en: 'Hide replies', fr: 'Masquer les réponses' },
30
+ 'MessagingThread.UNKNOWN_SENDER': { de: 'Unbekannte Person', en: 'Unknown person', fr: 'Personne inconnue' },
31
+ 'MessagingReadTicks.ALL_READ': { de: 'Von allen gelesen', en: 'Read by everyone', fr: 'Lu par tout le monde' },
32
+ 'MessagingReadTicks.DELIVERED': { de: 'Zugestellt an {{count}}', en: 'Delivered to {{count}}', fr: 'Distribué à {{count}}' },
33
+ 'MessagingComposer.LABEL': { de: 'Nachricht verfassen', en: 'Compose message', fr: 'Rédiger un message' },
34
+ 'MessagingComposer.MESSAGE': { de: 'Nachricht', en: 'Message', fr: 'Message' },
35
+ 'MessagingComposer.SEND': { de: 'Senden', en: 'Send', fr: 'Envoyer' },
36
+ 'MessagingComposer.RETRY': { de: 'Erneut versuchen', en: 'Retry', fr: 'Réessayer' },
37
+ 'MessagingComposer.ADD_ATTACHMENT': { de: 'Anhang hinzufügen', en: 'Add attachment', fr: 'Ajouter une pièce jointe' },
38
+ 'MessagingComposer.FILES_SELECTED': { de: '{{count}} Anhänge ausgewählt', en: '{{count}} attachments selected', fr: '{{count}} pièces jointes sélectionnées' },
39
+ 'MessagingComposer.REPLYING_TO': { de: 'Antwort an {{sender}}', en: 'Replying to {{sender}}', fr: 'Réponse à {{sender}}' },
40
+ 'MessagingComposer.SEND_ERROR': { de: 'Nachricht konnte nicht gesendet werden: {{message}}', en: 'Message could not be sent: {{message}}', fr: 'Impossible d’envoyer le message : {{message}}' },
41
+ 'MessagingComposer.UPLOAD_ERROR': { de: 'Anhang wurde abgelehnt: {{message}}', en: 'Attachment was rejected: {{message}}', fr: 'La pièce jointe a été rejetée : {{message}}' },
42
+ 'MessagingAttachments.LABEL': { de: 'Anhänge', en: 'Attachments', fr: 'Pièces jointes' },
43
+ 'MessagingAttachments.DOWNLOAD': { de: '{{name}} herunterladen', en: 'Download {{name}}', fr: 'Télécharger {{name}}' },
44
+ 'MessagingReactions.LABEL': { de: 'Reaktionen', en: 'Reactions', fr: 'Réactions' },
45
+ 'MessagingReactions.ADD': { de: 'Reaktion hinzufügen', en: 'Add reaction', fr: 'Ajouter une réaction' },
46
+ 'MessagingReactions.TOGGLE': { de: '{{emoji}} Reaktion mit {{count}} Stimmen umschalten', en: 'Toggle {{emoji}} reaction with {{count}} votes', fr: 'Basculer la réaction {{emoji}} avec {{count}} votes' },
47
+ 'MessagingReactions.ERROR': { de: 'Reaktion konnte nicht gespeichert werden: {{message}}', en: 'Reaction could not be saved: {{message}}', fr: 'Impossible d’enregistrer la réaction : {{message}}' },
48
+ 'MessagingPoll.LABEL': { de: 'Umfrage', en: 'Poll', fr: 'Sondage' },
49
+ 'MessagingPoll.VOTE': { de: 'Abstimmen', en: 'Vote', fr: 'Voter' },
50
+ 'MessagingPoll.CLOSE': { de: 'Umfrage schliessen', en: 'Close poll', fr: 'Clore le sondage' },
51
+ 'MessagingPoll.CLOSED': { de: 'Geschlossen', en: 'Closed', fr: 'Clos' },
52
+ 'MessagingPoll.SELECT_OPTION': { de: 'Wähle mindestens eine Option.', en: 'Select at least one option.', fr: 'Sélectionnez au moins une option.' },
53
+ 'MessagingPoll.VOTE_ERROR': { de: 'Stimme konnte nicht gespeichert werden: {{message}}', en: 'Vote could not be saved: {{message}}', fr: 'Impossible d’enregistrer le vote : {{message}}' },
54
+ 'MessagingPoll.CLOSE_ERROR': { de: 'Umfrage konnte nicht geschlossen werden: {{message}}', en: 'Poll could not be closed: {{message}}', fr: 'Impossible de clore le sondage : {{message}}' },
55
+ 'MessagingPoll.CREATE': { de: 'Umfrage erstellen', en: 'Create poll', fr: 'Créer un sondage' },
56
+ 'MessagingPoll.CREATE_ERROR': { de: 'Umfrage konnte nicht erstellt werden: {{message}}', en: 'Poll could not be created: {{message}}', fr: 'Impossible de créer le sondage : {{message}}' },
57
+ 'MessagingPoll.CREATE_VALIDATION': { de: 'Frage und mindestens zwei Optionen sind erforderlich.', en: 'A question and at least two options are required.', fr: 'Une question et au moins deux options sont requises.' },
58
+ 'MessagingPoll.QUESTION': { de: 'Frage', en: 'Question', fr: 'Question' },
59
+ 'MessagingPoll.OPTION': { de: 'Option {{count}}', en: 'Option {{count}}', fr: 'Option {{count}}' },
60
+ 'MessagingPoll.ADD_OPTION': { de: 'Option hinzufügen', en: 'Add option', fr: 'Ajouter une option' },
61
+ 'MessagingPoll.ALLOW_MULTIPLE': { de: 'Mehrfachauswahl erlauben', en: 'Allow multiple choices', fr: 'Autoriser plusieurs choix' },
62
+ 'MessagingPoll.CANCEL': { de: 'Abbrechen', en: 'Cancel', fr: 'Annuler' },
63
+ 'MessagingConfig.LABEL': { de: 'Nachrichten-Einstellungen', en: 'Messaging settings', fr: 'Paramètres de messagerie' },
64
+ 'MessagingConfig.TITLE': { de: 'Nachrichten-Einstellungen', en: 'Messaging settings', fr: 'Paramètres de messagerie' },
65
+ 'MessagingConfig.LOADING': { de: 'Einstellungen werden geladen…', en: 'Loading settings…', fr: 'Chargement des paramètres…' },
66
+ 'MessagingConfig.LOAD_ERROR': { de: 'Einstellungen konnten nicht geladen werden: {{message}}', en: 'Settings could not be loaded: {{message}}', fr: 'Impossible de charger les paramètres : {{message}}' },
67
+ 'MessagingConfig.SAVE_ERROR': { de: 'Einstellungen konnten nicht gespeichert werden: {{message}}', en: 'Settings could not be saved: {{message}}', fr: 'Impossible d’enregistrer les paramètres : {{message}}' },
68
+ 'MessagingConfig.DM_POLICY': { de: 'Direktnachrichten-Regel', en: 'Direct-message policy', fr: 'Règle des messages directs' },
69
+ 'MessagingConfig.DM_ALL': { de: 'Alle', en: 'Everyone', fr: 'Tout le monde' },
70
+ 'MessagingConfig.DM_SHARED': { de: 'Nur gemeinsame Gruppen', en: 'Shared groups only', fr: 'Groupes communs uniquement' },
71
+ 'MessagingConfig.DM_TEAM': { de: 'Nur Team', en: 'Team only', fr: 'Équipe uniquement' },
72
+ 'MessagingConfig.GROUP_ENABLED': { de: 'Gruppenchat aktivieren', en: 'Enable group chat', fr: 'Activer le chat de groupe' },
73
+ 'MessagingConfig.EVERYONE_POSTS': { de: 'Alle dürfen schreiben', en: 'Everyone can post', fr: 'Tout le monde peut publier' },
74
+ 'MessagingAnnouncement.CREATE': { de: 'Ankündigung erstellen', en: 'Create announcement', fr: 'Créer une annonce' },
75
+ 'MessagingAnnouncement.TITLE': { de: 'Titel', en: 'Title', fr: 'Titre' },
76
+ 'MessagingAnnouncement.BODY': { de: 'Text', en: 'Body', fr: 'Texte' },
77
+ 'MessagingAnnouncement.SEND': { de: 'Ankündigung senden', en: 'Send announcement', fr: 'Envoyer l’annonce' },
78
+ 'MessagingAnnouncement.CANCEL': { de: 'Abbrechen', en: 'Cancel', fr: 'Annuler' },
79
+ 'MessagingAnnouncement.VALIDATION': { de: 'Ein Titel ist erforderlich.', en: 'A title is required.', fr: 'Un titre est requis.' },
80
+ 'MessagingAnnouncement.ERROR': { de: 'Ankündigung konnte nicht gesendet werden: {{message}}', en: 'Announcement could not be sent: {{message}}', fr: 'Impossible d’envoyer l’annonce : {{message}}' },
81
+ };
package/dist/index.js CHANGED
@@ -27,9 +27,15 @@ export { RegistrationMethodsManager } from './components/RegistrationMethodsMana
27
27
  export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCard';
28
28
  export { AccessCodeSingleUseToggle } from './components/AccessCodeSingleUseToggle';
29
29
  export { QrSignupManager } from './components/QrSignupManager';
30
- // --- 6. Translations ---
30
+ // --- 6. Charts ---
31
+ export { ChartFrame } from './components/charts/ChartFrame';
32
+ export { BarChart } from './components/charts/BarChart';
33
+ export { LineChart } from './components/charts/LineChart';
34
+ export { getNeutralChartPalette, useNeutralChartPalette } from './components/charts/palette';
35
+ export { formatPercentage, formatCompact, formatRatio, createChartFormatters, useChartFormatters, } from './components/charts/formatters';
36
+ // --- 7. Translations ---
31
37
  export { authTranslations } from './i18n/authTranslations';
32
- // --- 7. Notifications ---
38
+ // --- 8. Notifications ---
33
39
  export { NotificationSettings } from './notifications/NotificationSettings';
34
40
  export * from './notifications/api';
35
41
  export { NotificationsProvider, useNotifications } from './notifications/NotificationsProvider';
@@ -37,7 +43,20 @@ export { useRealtime } from './notifications/realtime';
37
43
  export { NotificationBell } from './notifications/NotificationBell';
38
44
  export { PopupSurface } from './notifications/PopupSurface';
39
45
  export { getNotificationFeed, getUnreadCount, markNotifications } from './notifications/feedApi';
40
- // --- 8. Onboarding ---
46
+ // --- 9. Messaging ---
47
+ export * from './messaging/api';
48
+ export { MessagingProvider, useMessaging } from './messaging/MessagingProvider';
49
+ export { ConversationList } from './messaging/ConversationList';
50
+ export { ConversationLaunchers } from './messaging/ConversationLaunchers';
51
+ export { Thread } from './messaging/Thread';
52
+ export { MessageBubble } from './messaging/MessageBubble';
53
+ export { ReadTicks } from './messaging/ReadTicks';
54
+ export { Composer } from './messaging/Composer';
55
+ export { AttachmentList } from './messaging/AttachmentList';
56
+ export { ReactionBar } from './messaging/ReactionBar';
57
+ export { PollCard } from './messaging/PollCard';
58
+ export { MessagingScopeConfig } from './messaging/MessagingScopeConfig';
59
+ // --- 9. Onboarding ---
41
60
  export * from './onboarding/api';
42
61
  export { selectActiveSteps } from './onboarding/stepSelection';
43
62
  export { OnboardingContext, OnboardingProvider, UNIVERSAL_STEP_DESCRIPTORS, useOnboarding, } from './onboarding/OnboardingProvider';
@@ -46,6 +65,8 @@ export { CookieConsentStep } from './onboarding/steps/CookieConsentStep';
46
65
  export { CompleteNameStep } from './onboarding/steps/CompleteNameStep';
47
66
  export { BrowserPushStep } from './onboarding/steps/BrowserPushStep';
48
67
  export { PwaInstallStep } from './onboarding/steps/PwaInstallStep';
49
- // --- 9. Translations ---
68
+ // --- 10. Translations ---
50
69
  export { notificationsTranslations } from './i18n/notificationsTranslations';
51
70
  export { onboardingTranslations } from './i18n/onboardingTranslations';
71
+ export { chartsTranslations } from './i18n/chartsTranslations';
72
+ export { messagingTranslations } from './i18n/messagingTranslations';
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Button, Stack } from '@mui/material';
3
+ import { useEffect, useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useMessaging } from './MessagingProvider';
6
+ function isImage(attachment) { var _a; return (_a = attachment.content_type) === null || _a === void 0 ? void 0 : _a.startsWith('image/'); }
7
+ function nameOf(attachment) { return attachment.filename || attachment.name || attachment.id; }
8
+ /** Download-only attachment renderer; all bytes remain behind the authenticated REST endpoints. */
9
+ export function AttachmentList({ attachments = [] }) {
10
+ const { t } = useTranslation();
11
+ const { getAttachment, getAttachmentThumbnail } = useMessaging();
12
+ const [previews, setPreviews] = useState({});
13
+ useEffect(() => {
14
+ let live = true;
15
+ const urls = [];
16
+ attachments.filter(isImage).forEach(async (attachment) => {
17
+ try {
18
+ const blob = await getAttachmentThumbnail(attachment.id);
19
+ const url = blob instanceof Blob ? URL.createObjectURL(blob) : blob === null || blob === void 0 ? void 0 : blob.url;
20
+ if (url && live) {
21
+ urls.push(url);
22
+ setPreviews((current) => (Object.assign(Object.assign({}, current), { [attachment.id]: url })));
23
+ }
24
+ }
25
+ catch ( /* The image remains available through the download action. */_a) { /* The image remains available through the download action. */ }
26
+ });
27
+ return () => { live = false; urls.forEach((url) => URL.revokeObjectURL(url)); };
28
+ }, [attachments, getAttachmentThumbnail]);
29
+ if (!attachments.length)
30
+ return null;
31
+ const download = async (attachment) => {
32
+ const blob = await getAttachment(attachment.id);
33
+ if (!(blob instanceof Blob))
34
+ return;
35
+ const url = URL.createObjectURL(blob);
36
+ const link = document.createElement('a');
37
+ link.href = url;
38
+ link.download = nameOf(attachment);
39
+ link.click();
40
+ URL.revokeObjectURL(url);
41
+ };
42
+ return _jsx(Stack, { direction: "row", spacing: 1, flexWrap: "wrap", "aria-label": t('MessagingAttachments.LABEL'), children: attachments.map((attachment) => _jsxs(Button, { type: "button", size: "small", onClick: () => download(attachment), children: [previews[attachment.id] && _jsx(Box, { component: "img", src: previews[attachment.id], alt: "", width: 36, height: 36, sx: { objectFit: 'cover', mr: 0.75 } }), t('MessagingAttachments.DOWNLOAD', { name: nameOf(attachment) })] }, attachment.id)) });
43
+ }
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, IconButton, Stack, TextField, Typography } from '@mui/material';
3
+ import AttachFileIcon from '@mui/icons-material/AttachFile';
4
+ import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined';
5
+ import PollOutlinedIcon from '@mui/icons-material/PollOutlined';
6
+ import { useRef, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
9
+ function newRequestId() { var _a, _b; return ((_b = (_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === null || _b === void 0 ? void 0 : _b.call(_a)) || `message-${Date.now()}-${Math.random().toString(36).slice(2)}`; }
10
+ /**
11
+ * A provider-backed, independently mountable REST composer.
12
+ *
13
+ * `allowAnnouncement` and `linkTarget`: whether announcement composing (jg's
14
+ * `AnnouncementDialog` — title + body + a deep-link target) is offered here at
15
+ * all is a host decision (the server enforces who may actually post one; the
16
+ * client only offers the affordance). `linkTarget`'s VALUE is inherently
17
+ * app-specific navigation context (jg pre-fills it from the currently open
18
+ * event-info section) that ucm cannot compute itself, so the host supplies it.
19
+ */
20
+ export function Composer({ conversationId, replyTarget = null, onReplyTargetChange, allowAnnouncement = false, linkTarget }) {
21
+ var _a;
22
+ const { t } = useTranslation();
23
+ const { sendMessage, sendAttachments, createConversationPoll } = useMessaging();
24
+ const [body, setBody] = useState('');
25
+ const [files, setFiles] = useState([]);
26
+ const [error, setError] = useState(null);
27
+ const [failed, setFailed] = useState(null);
28
+ const [sending, setSending] = useState(false);
29
+ const [pollOpen, setPollOpen] = useState(false);
30
+ const [pollQuestion, setPollQuestion] = useState('');
31
+ const [pollOptions, setPollOptions] = useState(['', '']);
32
+ const [allowMultiple, setAllowMultiple] = useState(false);
33
+ const [announcementOpen, setAnnouncementOpen] = useState(false);
34
+ const [announcementTitle, setAnnouncementTitle] = useState('');
35
+ const [announcementBody, setAnnouncementBody] = useState('');
36
+ const inputRef = useRef(null);
37
+ const submit = async (requestId = newRequestId(), retry = false) => {
38
+ if (!body.trim() && !files.length)
39
+ return;
40
+ setSending(true);
41
+ setError(null);
42
+ const payload = { kind: 'chat', body: body.trim() || undefined, reply_to: (replyTarget === null || replyTarget === void 0 ? void 0 : replyTarget.id) || undefined, client_request_id: requestId };
43
+ try {
44
+ if (files.length) {
45
+ const formData = new FormData();
46
+ files.forEach((file) => formData.append('files[]', file));
47
+ if (payload.body)
48
+ formData.append('body', payload.body);
49
+ if (payload.reply_to)
50
+ formData.append('reply_to', String(payload.reply_to));
51
+ formData.append('client_request_id', requestId);
52
+ await sendAttachments(conversationId, formData, { clientRequestId: requestId, retry, optimisticMessage: { body: payload.body, reply_to: payload.reply_to, attachments: files.map((file, index) => ({ id: `local-${requestId}-${index}`, filename: file.name, content_type: file.type })) } });
53
+ }
54
+ else
55
+ await sendMessage(conversationId, payload, { clientRequestId: requestId, retry });
56
+ setBody('');
57
+ setFiles([]);
58
+ setFailed(null);
59
+ onReplyTargetChange === null || onReplyTargetChange === void 0 ? void 0 : onReplyTargetChange(null);
60
+ }
61
+ catch (sendError) {
62
+ const message = extractApiErrorMessage(sendError);
63
+ setError(files.length ? t('MessagingComposer.UPLOAD_ERROR', { message }) : t('MessagingComposer.SEND_ERROR', { message }));
64
+ setFailed({ requestId, retry: true });
65
+ }
66
+ finally {
67
+ setSending(false);
68
+ }
69
+ };
70
+ const submitPoll = async () => {
71
+ const options = pollOptions.map((option) => option.trim()).filter(Boolean);
72
+ if (!pollQuestion.trim() || options.length < 2) {
73
+ setError(t('MessagingPoll.CREATE_VALIDATION'));
74
+ return;
75
+ }
76
+ setSending(true);
77
+ setError(null);
78
+ try {
79
+ await createConversationPoll(conversationId, { question: pollQuestion.trim(), options, allow_multiple: allowMultiple }, { clientRequestId: newRequestId() });
80
+ setPollQuestion('');
81
+ setPollOptions(['', '']);
82
+ setAllowMultiple(false);
83
+ setPollOpen(false);
84
+ }
85
+ catch (pollError) {
86
+ setError(t('MessagingPoll.CREATE_ERROR', { message: extractApiErrorMessage(pollError) }));
87
+ }
88
+ finally {
89
+ setSending(false);
90
+ }
91
+ };
92
+ const submitAnnouncement = async () => {
93
+ if (!announcementTitle.trim()) {
94
+ setError(t('MessagingAnnouncement.VALIDATION'));
95
+ return;
96
+ }
97
+ setSending(true);
98
+ setError(null);
99
+ const requestId = newRequestId();
100
+ try {
101
+ await sendMessage(conversationId, {
102
+ kind: 'announcement',
103
+ title: announcementTitle.trim(),
104
+ body: announcementBody.trim() || undefined,
105
+ link_target: linkTarget || undefined,
106
+ client_request_id: requestId,
107
+ }, { clientRequestId: requestId });
108
+ setAnnouncementTitle('');
109
+ setAnnouncementBody('');
110
+ setAnnouncementOpen(false);
111
+ }
112
+ catch (announcementError) {
113
+ setError(t('MessagingAnnouncement.ERROR', { message: extractApiErrorMessage(announcementError) }));
114
+ }
115
+ finally {
116
+ setSending(false);
117
+ }
118
+ };
119
+ return _jsxs(Stack, { component: "form", spacing: 1, onSubmit: (event) => { event.preventDefault(); submit(); }, "aria-label": t('MessagingComposer.LABEL'), children: [replyTarget && _jsx(Alert, { severity: "info", onClose: () => onReplyTargetChange === null || onReplyTargetChange === void 0 ? void 0 : onReplyTargetChange(null), children: t('MessagingComposer.REPLYING_TO', { sender: ((_a = replyTarget.sender) === null || _a === void 0 ? void 0 : _a.display_name) || t('MessagingThread.UNKNOWN_SENDER') }) }), error && _jsx(Alert, { severity: "error", role: "alert", children: error }), files.length > 0 && _jsx(Typography, { variant: "caption", children: t('MessagingComposer.FILES_SELECTED', { count: files.length }) }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "flex-end", children: [_jsx("input", { ref: inputRef, hidden: true, type: "file", multiple: true, onChange: (event) => setFiles(Array.from(event.target.files || [])) }), _jsx(IconButton, { type: "button", "aria-label": t('MessagingComposer.ADD_ATTACHMENT'), onClick: () => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: _jsx(AttachFileIcon, {}) }), _jsx(IconButton, { type: "button", "aria-label": t('MessagingPoll.CREATE'), onClick: () => setPollOpen(true), children: _jsx(PollOutlinedIcon, {}) }), allowAnnouncement && _jsx(IconButton, { type: "button", "aria-label": t('MessagingAnnouncement.CREATE'), onClick: () => setAnnouncementOpen(true), children: _jsx(CampaignOutlinedIcon, {}) }), _jsx(TextField, { fullWidth: true, multiline: true, minRows: 2, value: body, onChange: (event) => setBody(event.target.value), label: t('MessagingComposer.MESSAGE'), disabled: sending }), _jsx(Button, { type: "submit", variant: "contained", disabled: sending || (!body.trim() && !files.length), children: t('MessagingComposer.SEND') })] }), failed && _jsx(Button, { type: "button", onClick: () => submit(failed.requestId, true), disabled: sending, children: t('MessagingComposer.RETRY') }), _jsxs(Dialog, { open: pollOpen, onClose: () => !sending && setPollOpen(false), fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('MessagingPoll.CREATE') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 1, sx: { pt: 1 }, children: [_jsx(TextField, { label: t('MessagingPoll.QUESTION'), value: pollQuestion, onChange: (event) => setPollQuestion(event.target.value), autoFocus: true }), pollOptions.map((option, index) => _jsx(TextField, { label: t('MessagingPoll.OPTION', { count: index + 1 }), value: option, onChange: (event) => setPollOptions((current) => current.map((value, optionIndex) => optionIndex === index ? event.target.value : value)) }, index)), _jsx(Button, { type: "button", onClick: () => setPollOptions((current) => [...current, '']), children: t('MessagingPoll.ADD_OPTION') }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: allowMultiple, onChange: (event) => setAllowMultiple(event.target.checked) }), label: t('MessagingPoll.ALLOW_MULTIPLE') })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setPollOpen(false), children: t('MessagingPoll.CANCEL') }), _jsx(Button, { variant: "contained", onClick: submitPoll, disabled: sending, children: t('MessagingPoll.CREATE') })] })] }), allowAnnouncement && _jsxs(Dialog, { open: announcementOpen, onClose: () => !sending && setAnnouncementOpen(false), fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('MessagingAnnouncement.CREATE') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 1, sx: { pt: 1 }, children: [_jsx(TextField, { label: t('MessagingAnnouncement.TITLE'), value: announcementTitle, onChange: (event) => setAnnouncementTitle(event.target.value), autoFocus: true }), _jsx(TextField, { label: t('MessagingAnnouncement.BODY'), value: announcementBody, onChange: (event) => setAnnouncementBody(event.target.value), multiline: true, minRows: 2 })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setAnnouncementOpen(false), children: t('MessagingAnnouncement.CANCEL') }), _jsx(Button, { variant: "contained", onClick: submitAnnouncement, disabled: sending, children: t('MessagingAnnouncement.SEND') })] })] })] });
120
+ }
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Alert, Button, Stack } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useMessaging } from './MessagingProvider';
6
+ /**
7
+ * Host apps provide launchable scopes. This component owns the REST transition
8
+ * that turns one into a normalized conversation, but deliberately does not
9
+ * discover groups itself: that is app-specific scope-picker data.
10
+ */
11
+ export function ConversationLaunchers({ groupLaunchers = [], broadcastLauncher, autoOpenBroadcast = false, onOpen }) {
12
+ const { t } = useTranslation();
13
+ const { openGroupConversation, openBroadcastConversation } = useMessaging();
14
+ const [error, setError] = useState(null);
15
+ const autoOpened = useRef(false);
16
+ const open = async (kind, payload) => {
17
+ setError(null);
18
+ try {
19
+ const conversation = kind === 'broadcast'
20
+ ? await openBroadcastConversation(payload)
21
+ : await openGroupConversation(payload);
22
+ onOpen === null || onOpen === void 0 ? void 0 : onOpen(conversation);
23
+ }
24
+ catch (_a) {
25
+ setError(t('MessagingLaunchers.OPEN_ERROR'));
26
+ }
27
+ };
28
+ useEffect(() => {
29
+ var _a;
30
+ if (!autoOpenBroadcast || !broadcastLauncher || autoOpened.current)
31
+ return;
32
+ autoOpened.current = true;
33
+ open('broadcast', (_a = broadcastLauncher.payload) !== null && _a !== void 0 ? _a : broadcastLauncher);
34
+ }, [autoOpenBroadcast, broadcastLauncher]); // one intentional auto-open per mounted launcher
35
+ return (_jsxs(Stack, { spacing: 1, children: [broadcastLauncher && !autoOpenBroadcast && (_jsx(Button, { variant: "outlined", onClick: () => { var _a; return open('broadcast', (_a = broadcastLauncher.payload) !== null && _a !== void 0 ? _a : broadcastLauncher); }, children: broadcastLauncher.label || t('MessagingLaunchers.OPEN_BROADCAST') })), groupLaunchers.map((launcher) => {
36
+ var _a;
37
+ return (_jsx(Button, { variant: "outlined", onClick: () => { var _a; return open('group', (_a = launcher.payload) !== null && _a !== void 0 ? _a : launcher); }, children: launcher.label || t('MessagingLaunchers.OPEN_GROUP') }, (_a = launcher.id) !== null && _a !== void 0 ? _a : launcher.label));
38
+ }), error && _jsx(Alert, { severity: "error", children: error })] }));
39
+ }
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { Archive, Campaign, Groups, MoreVert, NotificationsOff, Person } from '@mui/icons-material';
4
+ import { Badge, Box, Button, CircularProgress, IconButton, List, ListItemButton, ListItemText, Menu, MenuItem, Stack, Typography } from '@mui/material';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useMessaging } from './MessagingProvider';
7
+ import { ConversationLaunchers } from './ConversationLaunchers';
8
+ function titleOf(conversation, t) { var _a, _b; return conversation.title || ((_a = conversation.other_user) === null || _a === void 0 ? void 0 : _a.display_name) || ((_b = conversation.other_user) === null || _b === void 0 ? void 0 : _b.username) || t('MessagingList.UNTITLED'); }
9
+ function ordered(conversations) {
10
+ return [...conversations].sort((left, right) => new Date(right.last_message_at || 0) - new Date(left.last_message_at || 0));
11
+ }
12
+ // jg's ConversationList renders a distinct icon per conversation kind so a
13
+ // user can identify type/person at a glance (getConversationAvatar); that
14
+ // glanceable identification is a feature, not layout, so it's reproduced
15
+ // here rather than left as a generic archive-status glyph.
16
+ function KindIcon({ conversation }) {
17
+ if (conversation.kind === 'broadcast')
18
+ return _jsx(Campaign, { fontSize: "small" });
19
+ if (conversation.kind === 'group' || conversation.kind === 'managed' || conversation.kind === 'object_thread')
20
+ return _jsx(Groups, { fontSize: "small" });
21
+ return _jsx(Person, { fontSize: "small" });
22
+ }
23
+ /** A standalone, provider-backed list; scope picker metadata is supplied as launcher props. */
24
+ export function ConversationList({ onOpen, groupLaunchers, broadcastLauncher, autoOpenBroadcast = false, includeArchived = false }) {
25
+ const { t } = useTranslation();
26
+ const { cache, activeConversationId, loadMoreConversations, setConversationArchived, setConversationPreferences } = useMessaging();
27
+ const [menuConversation, setMenuConversation] = useState(null);
28
+ const [menuAnchor, setMenuAnchor] = useState(null);
29
+ const [loadingMore, setLoadingMore] = useState(false);
30
+ const [error, setError] = useState(null);
31
+ const conversations = useMemo(() => ordered(Object.values(cache.conversations).filter((item) => includeArchived || !item.archived_at && !item.archived)), [cache.conversations, includeArchived]);
32
+ const unreadFor = (conversation) => { var _a, _b, _c; return (_c = (_a = conversation.unread_count) !== null && _a !== void 0 ? _a : (_b = cache.unread.by_conversation) === null || _b === void 0 ? void 0 : _b[conversation.id]) !== null && _c !== void 0 ? _c : 0; };
33
+ // Once opened, the broadcast conversation becomes a normal, recency-sorted
34
+ // row with real unread/preview state — hide the static launcher so it isn't
35
+ // shown twice (button + list row) with the button's stale, unread-less copy.
36
+ const broadcastAlreadyOpened = useMemo(() => Object.values(cache.conversations).some((item) => item.kind === 'broadcast'), [cache.conversations]);
37
+ const loadMore = async () => {
38
+ setLoadingMore(true);
39
+ setError(null);
40
+ try {
41
+ await loadMoreConversations();
42
+ }
43
+ catch (_a) {
44
+ setError(t('MessagingList.LOAD_ERROR'));
45
+ }
46
+ finally {
47
+ setLoadingMore(false);
48
+ }
49
+ };
50
+ const closeMenu = () => { setMenuConversation(null); setMenuAnchor(null); };
51
+ const archive = async () => {
52
+ try {
53
+ await setConversationArchived(menuConversation.id, true);
54
+ }
55
+ catch (_a) {
56
+ setError(t('MessagingList.ARCHIVE_ERROR'));
57
+ }
58
+ closeMenu();
59
+ };
60
+ const toggleMute = async () => {
61
+ try {
62
+ await setConversationPreferences(menuConversation.id, { muted: !menuConversation.muted });
63
+ }
64
+ catch (_a) {
65
+ setError(t('MessagingList.PREFERENCES_ERROR'));
66
+ }
67
+ closeMenu();
68
+ };
69
+ return (_jsxs(Stack, { spacing: 1, children: [_jsx(ConversationLaunchers, { groupLaunchers: groupLaunchers, broadcastLauncher: broadcastAlreadyOpened ? null : broadcastLauncher, autoOpenBroadcast: autoOpenBroadcast && !broadcastAlreadyOpened, onOpen: onOpen }), error && _jsx(Typography, { color: "error", role: "alert", children: error }), !conversations.length ? _jsx(Box, { sx: { py: 3, textAlign: 'center' }, children: _jsx(Typography, { color: "text.secondary", children: t('MessagingList.EMPTY') }) }) : (_jsx(List, { disablePadding: true, "aria-label": t('MessagingList.LABEL'), children: conversations.map((conversation) => {
70
+ var _a;
71
+ const unread = unreadFor(conversation);
72
+ const archived = Boolean(conversation.archived || conversation.archived_at);
73
+ return _jsxs(ListItemButton, { selected: conversation.id === activeConversationId, onClick: () => onOpen === null || onOpen === void 0 ? void 0 : onOpen(conversation), sx: { minHeight: 64, opacity: archived ? 0.6 : 1 }, children: [_jsx(Badge, { color: "error", badgeContent: unread || null, max: 99, sx: { mr: 2 }, children: archived ? _jsx(Archive, { fontSize: "small", color: "disabled" }) : _jsx(KindIcon, { conversation: conversation }) }), _jsx(ListItemText, { primary: titleOf(conversation, t), secondary: ((_a = conversation.last_message) === null || _a === void 0 ? void 0 : _a.body) || t('MessagingList.NO_MESSAGES'), primaryTypographyProps: { fontWeight: unread ? 700 : 400, noWrap: true }, secondaryTypographyProps: { noWrap: true } }), _jsx(IconButton, { "aria-label": t('MessagingList.ACTIONS'), onClick: (event) => { event.stopPropagation(); setMenuAnchor(event.currentTarget); setMenuConversation(conversation); }, children: _jsx(MoreVert, {}) })] }, conversation.id);
74
+ }) })), cache.cursors.conversations && _jsx(Button, { onClick: loadMore, disabled: loadingMore, children: loadingMore ? _jsx(CircularProgress, { size: 18 }) : t('MessagingList.LOAD_MORE') }), _jsxs(Menu, { anchorEl: menuAnchor, open: Boolean(menuConversation), onClose: closeMenu, children: [_jsxs(MenuItem, { onClick: toggleMute, children: [_jsx(NotificationsOff, { fontSize: "small", sx: { mr: 1 } }), (menuConversation === null || menuConversation === void 0 ? void 0 : menuConversation.muted) ? t('MessagingList.UNMUTE') : t('MessagingList.MUTE')] }), _jsxs(MenuItem, { onClick: archive, children: [_jsx(Archive, { fontSize: "small", sx: { mr: 1 } }), t('MessagingList.ARCHIVE')] })] })] }));
75
+ }
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Paper, Stack, Typography } from '@mui/material';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { AttachmentList } from './AttachmentList';
5
+ import { ReactionBar } from './ReactionBar';
6
+ import { PollCard } from './PollCard';
7
+ function senderName(message) {
8
+ var _a, _b, _c;
9
+ return ((_a = message.sender) === null || _a === void 0 ? void 0 : _a.display_name) || ((_b = message.sender) === null || _b === void 0 ? void 0 : _b.username) || message.sender_name || ((_c = message.sender) === null || _c === void 0 ? void 0 : _c.name);
10
+ }
11
+ /** Provider-backed timeline collaborator; it deliberately has no mutation controls. */
12
+ export function MessageBubble({ message, replyTo, onReply, children }) {
13
+ const { t } = useTranslation();
14
+ const deleted = Boolean(message.deleted_at);
15
+ const attachments = message.attachments || [];
16
+ return (_jsx(Paper, { component: "article", variant: "outlined", sx: { p: 1.25, maxWidth: 'min(100%, 680px)' }, "aria-label": t('MessagingThread.MESSAGE'), children: _jsxs(Stack, { spacing: 0.75, children: [replyTo && _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('MessagingThread.REPLY_TO', { sender: senderName(replyTo) || t('MessagingThread.UNKNOWN_SENDER') }) }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "baseline", children: [_jsx(Typography, { variant: "subtitle2", children: senderName(message) || t('MessagingThread.UNKNOWN_SENDER') }), message.created_at && _jsx(Typography, { variant: "caption", color: "text.secondary", children: new Date(message.created_at).toLocaleString() }), message.edited_at && !deleted && _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('MessagingThread.EDITED') })] }), !deleted && message.kind === 'announcement' && message.title && _jsx(Typography, { variant: "subtitle1", fontWeight: 700, children: message.title }), _jsx(Typography, { sx: { whiteSpace: 'pre-wrap' }, children: deleted ? t('MessagingThread.DELETED') : (message.kind === 'announcement' ? message.body : (message.body || message.title)) || '' }), !deleted && _jsxs(Stack, { spacing: 0.75, children: [attachments.length > 0 && _jsx(AttachmentList, { attachments: attachments }), _jsx(ReactionBar, { message: message }), message.poll && _jsx(PollCard, { message: message })] }), !deleted && onReply && _jsx(Button, { type: "button", size: "small", onClick: () => onReply(message), children: t('MessagingThread.REPLY') }), children] }) }));
17
+ }