@micha.bigler/ui-core-micha 3.14.3 → 4.0.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.
- package/CHANGELOG.md +31 -0
- package/README.md +7 -0
- package/dist/auth/UserMenu.js +1 -1
- package/dist/auth/apiClient.js +7 -6
- package/dist/chat/ChatList.js +1 -1
- package/dist/chat/ChatThread.js +2 -1
- package/dist/chat/ChatWorkspace.js +1 -1
- package/dist/chat/ModelPicker.js +2 -5
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/chat/useChatSocket.js +1 -1
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/SecurityComponent.js +1 -1
- package/dist/components/UserListComponent.js +2 -8
- package/dist/components/charts/ChartFrame.js +1 -1
- package/dist/components/charts/ScatterChart.js +2 -1
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/layout/SectionNav.js +1 -1
- package/dist/messaging/AttachmentList.js +2 -2
- package/dist/messaging/Composer.js +2 -1
- package/dist/messaging/ConversationLaunchers.js +1 -4
- package/dist/messaging/ConversationList.js +1 -1
- package/dist/messaging/DirectMessageLauncher.js +1 -1
- package/dist/messaging/MessagingProvider.js +0 -0
- package/dist/messaging/PollCard.js +7 -5
- package/dist/messaging/ReadTicks.js +2 -2
- package/dist/messaging/Thread.js +5 -3
- package/dist/notifications/NotificationBell.js +4 -7
- package/dist/notifications/NotificationSettings.js +1 -1
- package/dist/onboarding/OnboardingProvider.js +3 -6
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +2 -3
- package/dist/pages/PasswordChangePage.js +2 -3
- package/dist/pages/PasswordInvitePage.js +2 -3
- package/dist/pages/PasswordResetRequestPage.js +2 -3
- package/dist/pages/SignUpPage.js +2 -3
- package/dist/pages/SignupConfirmPage.js +2 -3
- package/dist/theme/createAppTheme.js +5 -4
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/dist/theme/themeCompleteness.js +3 -12
- package/dist/utils/auth-errors.js +4 -3
- package/package.json +41 -16
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -148
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { Alert, Box, Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, IconButton, LinearProgress, Popover, Stack, TextField, Typography } from '@mui/material';
|
|
2
|
-
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
|
3
|
-
import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined';
|
|
4
|
-
import CloseIcon from '@mui/icons-material/Close';
|
|
5
|
-
import EmojiEmotionsOutlinedIcon from '@mui/icons-material/EmojiEmotionsOutlined';
|
|
6
|
-
import PollOutlinedIcon from '@mui/icons-material/PollOutlined';
|
|
7
|
-
import { useEffect, useRef, useState } from 'react';
|
|
8
|
-
import { useTranslation } from 'react-i18next';
|
|
9
|
-
|
|
10
|
-
import { compressImageForUpload } from './composerImageCompression';
|
|
11
|
-
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
12
|
-
|
|
13
|
-
export const COMPOSER_EMOJI_BUTTON_SX = { fontSize: '1.15rem' };
|
|
14
|
-
export const COMPOSER_MESSAGE_FIELD_SX = { flex: 1, minWidth: 120 };
|
|
15
|
-
export const COMPOSER_SEND_BUTTON_SX = { flexShrink: 0 };
|
|
16
|
-
|
|
17
|
-
// A wider, Composer-only set (MSG-6i) -- deliberately separate from
|
|
18
|
-
// ReactionBar's own QUICK_EMOJIS (5, curated for fast reactions). Mobile
|
|
19
|
-
// users already have a full native emoji keyboard for free-form text; this
|
|
20
|
-
// exists for desktop, where that's far less discoverable. Not a search
|
|
21
|
-
// picker -- a fixed curated grid, easy to extend later.
|
|
22
|
-
const COMPOSER_EMOJIS = [
|
|
23
|
-
'😀', '😂', '😊', '😍', '😉', '😢', '😮', '😡',
|
|
24
|
-
'👍', '👎', '👏', '🙏', '💪', '🤝', '✌️', '👋',
|
|
25
|
-
'❤️', '💕', '🔥', '🎉', '🎈', '⭐', '✅', '❌',
|
|
26
|
-
'☀️', '🌧️', '⚽', '🍕',
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
function newRequestId() { return globalThis.crypto?.randomUUID?.() || `message-${Date.now()}-${Math.random().toString(36).slice(2)}`; }
|
|
30
|
-
function isImage(file) { return file.type?.startsWith('image/'); }
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* A provider-backed, independently mountable REST composer.
|
|
34
|
-
*
|
|
35
|
-
* `allowAnnouncement` and `linkTarget`: whether announcement composing (jg's
|
|
36
|
-
* `AnnouncementDialog` — title + body + a deep-link target) is offered here at
|
|
37
|
-
* all is a host decision (the server enforces who may actually post one; the
|
|
38
|
-
* client only offers the affordance). `linkTarget`'s VALUE is inherently
|
|
39
|
-
* app-specific navigation context (jg pre-fills it from the currently open
|
|
40
|
-
* event-info section) that ucm cannot compute itself, so the host supplies it.
|
|
41
|
-
* Broadcasting to a 1:1 direct conversation is never meaningful regardless of
|
|
42
|
-
* what the host passes for `allowAnnouncement` (typically an event-manager
|
|
43
|
-
* permission check with no notion of the currently open conversation's kind),
|
|
44
|
-
* so this is excluded here rather than relying on every host to remember it.
|
|
45
|
-
*/
|
|
46
|
-
export function Composer({ conversationId, conversation, replyTarget = null, onReplyTargetChange, allowAnnouncement = false, linkTarget }) {
|
|
47
|
-
const canAnnounce = allowAnnouncement && conversation?.kind !== 'direct';
|
|
48
|
-
const { t } = useTranslation();
|
|
49
|
-
const { sendMessage, sendAttachments, createConversationPoll } = useMessaging();
|
|
50
|
-
const [body, setBody] = useState('');
|
|
51
|
-
const [files, setFiles] = useState([]);
|
|
52
|
-
const [error, setError] = useState(null);
|
|
53
|
-
const [failed, setFailed] = useState(null);
|
|
54
|
-
const [sending, setSending] = useState(false);
|
|
55
|
-
const [pollOpen, setPollOpen] = useState(false);
|
|
56
|
-
const [pollQuestion, setPollQuestion] = useState('');
|
|
57
|
-
const [pollOptions, setPollOptions] = useState(['', '']);
|
|
58
|
-
const [allowMultiple, setAllowMultiple] = useState(false);
|
|
59
|
-
const [announcementOpen, setAnnouncementOpen] = useState(false);
|
|
60
|
-
const [announcementTitle, setAnnouncementTitle] = useState('');
|
|
61
|
-
const [announcementBody, setAnnouncementBody] = useState('');
|
|
62
|
-
const [emojiAnchor, setEmojiAnchor] = useState(null);
|
|
63
|
-
const [previews, setPreviews] = useState([]);
|
|
64
|
-
const [uploadProgress, setUploadProgress] = useState(null);
|
|
65
|
-
const inputRef = useRef(null);
|
|
66
|
-
const messageInputRef = useRef(null);
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (replyTarget?.deleted_at) onReplyTargetChange?.(null);
|
|
69
|
-
}, [replyTarget, onReplyTargetChange]);
|
|
70
|
-
useEffect(() => { setBody(''); setFiles([]); }, [conversationId]);
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
const urls = files.map((file) => isImage(file) ? URL.createObjectURL(file) : null);
|
|
73
|
-
setPreviews(urls);
|
|
74
|
-
return () => urls.filter(Boolean).forEach((url) => URL.revokeObjectURL(url));
|
|
75
|
-
}, [files]);
|
|
76
|
-
const submit = async (requestId = newRequestId(), retry = false) => {
|
|
77
|
-
if (!body.trim() && !files.length) return;
|
|
78
|
-
setSending(true); setError(null);
|
|
79
|
-
const payload = { kind: 'chat', body: body.trim() || undefined, reply_to: replyTarget?.id || undefined, client_request_id: requestId };
|
|
80
|
-
try {
|
|
81
|
-
if (files.length) {
|
|
82
|
-
const formData = new FormData();
|
|
83
|
-
const uploadFiles = await Promise.all(files.map(compressImageForUpload));
|
|
84
|
-
uploadFiles.forEach((file) => formData.append('files[]', file));
|
|
85
|
-
if (payload.body) formData.append('body', payload.body);
|
|
86
|
-
if (payload.reply_to) formData.append('reply_to', String(payload.reply_to));
|
|
87
|
-
formData.append('client_request_id', requestId);
|
|
88
|
-
setUploadProgress(0);
|
|
89
|
-
const onUploadProgress = (event) => setUploadProgress(event.total ? Math.round((event.loaded / event.total) * 100) : null);
|
|
90
|
-
await sendAttachments(conversationId, formData, { clientRequestId: requestId, retry, onUploadProgress, 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 })) } });
|
|
91
|
-
} else await sendMessage(conversationId, payload, { clientRequestId: requestId, retry });
|
|
92
|
-
setBody(''); setFiles([]); setFailed(null); onReplyTargetChange?.(null);
|
|
93
|
-
} catch (sendError) {
|
|
94
|
-
const message = extractApiErrorMessage(sendError);
|
|
95
|
-
setError(files.length ? t('MessagingComposer.UPLOAD_ERROR', { message }) : t('MessagingComposer.SEND_ERROR', { message }));
|
|
96
|
-
setFailed({ requestId, retry: true });
|
|
97
|
-
} finally { setSending(false); setUploadProgress(null); }
|
|
98
|
-
};
|
|
99
|
-
const removeFile = (index) => setFiles((current) => current.filter((_, fileIndex) => fileIndex !== index));
|
|
100
|
-
const insertEmoji = (emoji) => {
|
|
101
|
-
const input = messageInputRef.current?.querySelector?.('textarea') || messageInputRef.current;
|
|
102
|
-
const start = input?.selectionStart ?? body.length;
|
|
103
|
-
const end = input?.selectionEnd ?? start;
|
|
104
|
-
setBody(`${body.slice(0, start)}${emoji}${body.slice(end)}`); setEmojiAnchor(null);
|
|
105
|
-
requestAnimationFrame(() => { input?.focus(); input?.setSelectionRange(start + emoji.length, start + emoji.length); });
|
|
106
|
-
};
|
|
107
|
-
const submitPoll = async () => {
|
|
108
|
-
const options = pollOptions.map((option) => option.trim()).filter(Boolean);
|
|
109
|
-
if (!pollQuestion.trim() || options.length < 2) { setError(t('MessagingPoll.CREATE_VALIDATION')); return; }
|
|
110
|
-
setSending(true); setError(null);
|
|
111
|
-
try {
|
|
112
|
-
await createConversationPoll(conversationId, { question: pollQuestion.trim(), options, allow_multiple: allowMultiple }, { clientRequestId: newRequestId() });
|
|
113
|
-
setPollQuestion(''); setPollOptions(['', '']); setAllowMultiple(false); setPollOpen(false);
|
|
114
|
-
} catch (pollError) { setError(t('MessagingPoll.CREATE_ERROR', { message: extractApiErrorMessage(pollError) })); } finally { setSending(false); }
|
|
115
|
-
};
|
|
116
|
-
const submitAnnouncement = async () => {
|
|
117
|
-
if (!announcementTitle.trim()) { setError(t('MessagingAnnouncement.VALIDATION')); return; }
|
|
118
|
-
setSending(true); setError(null);
|
|
119
|
-
const requestId = newRequestId();
|
|
120
|
-
try {
|
|
121
|
-
await sendMessage(conversationId, {
|
|
122
|
-
kind: 'announcement',
|
|
123
|
-
title: announcementTitle.trim(),
|
|
124
|
-
body: announcementBody.trim() || undefined,
|
|
125
|
-
link_target: linkTarget || undefined,
|
|
126
|
-
client_request_id: requestId,
|
|
127
|
-
}, { clientRequestId: requestId });
|
|
128
|
-
setAnnouncementTitle(''); setAnnouncementBody(''); setAnnouncementOpen(false);
|
|
129
|
-
} catch (announcementError) { setError(t('MessagingAnnouncement.ERROR', { message: extractApiErrorMessage(announcementError) })); } finally { setSending(false); }
|
|
130
|
-
};
|
|
131
|
-
return <Stack component="form" spacing={1} onSubmit={(event) => { event.preventDefault(); submit(); }} aria-label={t('MessagingComposer.LABEL')}>
|
|
132
|
-
{replyTarget && <Alert severity="info" onClose={() => onReplyTargetChange?.(null)}>{t('MessagingComposer.REPLYING_TO', { sender: replyTarget.sender?.display_name || t('MessagingThread.UNKNOWN_SENDER') })}</Alert>}
|
|
133
|
-
{error && <Alert severity="error" role="alert">{error}</Alert>}
|
|
134
|
-
{files.length > 0 && <Stack direction="row" spacing={0.5} flexWrap="wrap" aria-label={t('MessagingComposer.STAGED_FILES')}>
|
|
135
|
-
{files.map((file, index) => <Stack key={`${file.name}-${index}`} direction="row" alignItems="center" spacing={0.25} sx={{ border: 1, borderColor: 'divider', borderRadius: 1, p: 0.25 }}>
|
|
136
|
-
{previews[index] && <Box component="img" src={previews[index]} alt={file.name} width={36} height={36} sx={{ objectFit: 'cover' }} />}
|
|
137
|
-
<Typography variant="caption">{file.name}</Typography><IconButton type="button" size="small" aria-label={t('MessagingComposer.REMOVE_ATTACHMENT', { name: file.name })} onClick={() => removeFile(index)}><CloseIcon fontSize="inherit" /></IconButton>
|
|
138
|
-
</Stack>)}
|
|
139
|
-
</Stack>}
|
|
140
|
-
{files.length > 0 && <Typography variant="caption">{t('MessagingComposer.FILES_SELECTED', { count: files.length })}</Typography>}
|
|
141
|
-
<Stack direction="row" spacing={1}>
|
|
142
|
-
<input ref={inputRef} hidden type="file" multiple onChange={(event) => setFiles(Array.from(event.target.files || []))} />
|
|
143
|
-
<IconButton type="button" aria-label={t('MessagingComposer.ADD_ATTACHMENT')} onClick={() => inputRef.current?.click()}><AttachFileIcon /></IconButton>
|
|
144
|
-
<IconButton type="button" aria-label={t('MessagingComposer.ADD_EMOJI')} onClick={(event) => setEmojiAnchor(event.currentTarget)}><EmojiEmotionsOutlinedIcon /></IconButton>
|
|
145
|
-
<IconButton type="button" aria-label={t('MessagingPoll.CREATE')} disabled={pollOpen || files.some(isImage)} onClick={() => setPollOpen(true)}><PollOutlinedIcon /></IconButton>
|
|
146
|
-
{canAnnounce && <IconButton type="button" aria-label={t('MessagingAnnouncement.CREATE')} onClick={() => setAnnouncementOpen(true)}><CampaignOutlinedIcon /></IconButton>}
|
|
147
|
-
</Stack>
|
|
148
|
-
<Stack direction="row" spacing={1} alignItems="flex-end">
|
|
149
|
-
<TextField inputRef={messageInputRef} fullWidth multiline minRows={2} value={body} onChange={(event) => setBody(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); submit(); } }} label={t('MessagingComposer.MESSAGE')} disabled={sending} sx={COMPOSER_MESSAGE_FIELD_SX} />
|
|
150
|
-
<Button type="submit" variant="contained" disabled={sending || (!body.trim() && !files.length)} sx={COMPOSER_SEND_BUTTON_SX}>{t('MessagingComposer.SEND')}</Button>
|
|
151
|
-
</Stack>
|
|
152
|
-
{sending && files.length > 0 && <Stack spacing={0.5} role="status">
|
|
153
|
-
<Stack direction="row" spacing={1} alignItems="center">
|
|
154
|
-
{uploadProgress == null ? <CircularProgress size={16} /> : <CircularProgress size={16} variant="determinate" value={uploadProgress} />}
|
|
155
|
-
<Typography variant="caption">{t('MessagingComposer.UPLOADING')}</Typography>
|
|
156
|
-
</Stack>
|
|
157
|
-
{uploadProgress != null && <LinearProgress variant="determinate" value={uploadProgress} />}
|
|
158
|
-
</Stack>}
|
|
159
|
-
{failed && <Button type="button" onClick={() => submit(failed.requestId, true)} disabled={sending}>{t('MessagingComposer.RETRY')}</Button>}
|
|
160
|
-
<Popover
|
|
161
|
-
anchorEl={emojiAnchor}
|
|
162
|
-
open={Boolean(emojiAnchor)}
|
|
163
|
-
onClose={() => setEmojiAnchor(null)}
|
|
164
|
-
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
165
|
-
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
|
166
|
-
>
|
|
167
|
-
{/*
|
|
168
|
-
A Popover, not a Menu: this is a grid of plain buttons, not a linear
|
|
169
|
-
list of menuitems, so MUI's Menu/MenuList roving-tabindex arrow-key
|
|
170
|
-
navigation does not apply here and (via role="menu") would advertise
|
|
171
|
-
semantics the grid can't deliver. Popover leaves keyboard traversal to
|
|
172
|
-
the browser's native Tab order, which does work for a button grid.
|
|
173
|
-
*/}
|
|
174
|
-
<Box role="group" aria-label={t('MessagingComposer.EMOJI_PICKER')} sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 0.25, p: 0.5, maxWidth: 280 }}>
|
|
175
|
-
{COMPOSER_EMOJIS.map((emoji) => (
|
|
176
|
-
<IconButton key={emoji} type="button" size="small" onClick={() => insertEmoji(emoji)} sx={COMPOSER_EMOJI_BUTTON_SX}>
|
|
177
|
-
{emoji}
|
|
178
|
-
</IconButton>
|
|
179
|
-
))}
|
|
180
|
-
</Box>
|
|
181
|
-
</Popover>
|
|
182
|
-
<Dialog open={pollOpen} onClose={() => !sending && setPollOpen(false)} fullWidth maxWidth="sm">
|
|
183
|
-
<DialogTitle>{t('MessagingPoll.CREATE')}</DialogTitle><DialogContent><Stack spacing={1} sx={{ pt: 1 }}>
|
|
184
|
-
<TextField label={t('MessagingPoll.QUESTION')} value={pollQuestion} onChange={(event) => setPollQuestion(event.target.value)} autoFocus />
|
|
185
|
-
{pollOptions.map((option, index) => <TextField key={index} label={t('MessagingPoll.OPTION', { count: index + 1 })} value={option} onChange={(event) => setPollOptions((current) => current.map((value, optionIndex) => optionIndex === index ? event.target.value : value))} />)}
|
|
186
|
-
<Button type="button" disabled={pollOptions.length >= 10} onClick={() => setPollOptions((current) => [...current, ''])}>{t('MessagingPoll.ADD_OPTION')}</Button>
|
|
187
|
-
<FormControlLabel control={<Checkbox checked={allowMultiple} onChange={(event) => setAllowMultiple(event.target.checked)} />} label={t('MessagingPoll.ALLOW_MULTIPLE')} />
|
|
188
|
-
</Stack></DialogContent><DialogActions><Button onClick={() => setPollOpen(false)}>{t('MessagingPoll.CANCEL')}</Button><Button variant="contained" onClick={submitPoll} disabled={sending}>{t('MessagingPoll.CREATE')}</Button></DialogActions>
|
|
189
|
-
</Dialog>
|
|
190
|
-
{canAnnounce && <Dialog open={announcementOpen} onClose={() => !sending && setAnnouncementOpen(false)} fullWidth maxWidth="sm">
|
|
191
|
-
<DialogTitle>{t('MessagingAnnouncement.CREATE')}</DialogTitle><DialogContent><Stack spacing={1} sx={{ pt: 1 }}>
|
|
192
|
-
<TextField label={t('MessagingAnnouncement.TITLE')} value={announcementTitle} onChange={(event) => setAnnouncementTitle(event.target.value)} autoFocus />
|
|
193
|
-
<TextField label={t('MessagingAnnouncement.BODY')} value={announcementBody} onChange={(event) => setAnnouncementBody(event.target.value)} multiline minRows={2} />
|
|
194
|
-
</Stack></DialogContent><DialogActions><Button onClick={() => setAnnouncementOpen(false)}>{t('MessagingAnnouncement.CANCEL')}</Button><Button variant="contained" onClick={submitAnnouncement} disabled={sending}>{t('MessagingAnnouncement.SEND')}</Button></DialogActions>
|
|
195
|
-
</Dialog>}
|
|
196
|
-
</Stack>;
|
|
197
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Alert, Button, Stack } from '@mui/material';
|
|
3
|
-
import { useTranslation } from 'react-i18next';
|
|
4
|
-
|
|
5
|
-
import { useMessaging } from './MessagingProvider';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Host apps provide launchable scopes. This component owns the REST transition
|
|
9
|
-
* that turns one into a normalized conversation, but deliberately does not
|
|
10
|
-
* discover groups itself: that is app-specific scope-picker data.
|
|
11
|
-
*/
|
|
12
|
-
export function ConversationLaunchers({ groupLaunchers = [], broadcastLauncher, autoOpenBroadcast = false, onOpen }) {
|
|
13
|
-
const { t } = useTranslation();
|
|
14
|
-
const { openGroupConversation, openBroadcastConversation } = useMessaging();
|
|
15
|
-
const [error, setError] = useState(null);
|
|
16
|
-
const autoOpened = useRef(false);
|
|
17
|
-
|
|
18
|
-
const open = async (kind, payload) => {
|
|
19
|
-
setError(null);
|
|
20
|
-
try {
|
|
21
|
-
const conversation = kind === 'broadcast'
|
|
22
|
-
? await openBroadcastConversation(payload)
|
|
23
|
-
: await openGroupConversation(payload);
|
|
24
|
-
onOpen?.(conversation);
|
|
25
|
-
} catch {
|
|
26
|
-
setError(t('MessagingLaunchers.OPEN_ERROR'));
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!autoOpenBroadcast || !broadcastLauncher || autoOpened.current) return;
|
|
32
|
-
autoOpened.current = true;
|
|
33
|
-
open('broadcast', broadcastLauncher.payload ?? broadcastLauncher);
|
|
34
|
-
}, [autoOpenBroadcast, broadcastLauncher]); // one intentional auto-open per mounted launcher
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<Stack spacing={1}>
|
|
38
|
-
{broadcastLauncher && !autoOpenBroadcast && (
|
|
39
|
-
<Button variant="outlined" onClick={() => open('broadcast', broadcastLauncher.payload ?? broadcastLauncher)}>
|
|
40
|
-
{broadcastLauncher.label || t('MessagingLaunchers.OPEN_BROADCAST')}
|
|
41
|
-
</Button>
|
|
42
|
-
)}
|
|
43
|
-
{groupLaunchers.map((launcher) => (
|
|
44
|
-
<Button key={launcher.id ?? launcher.label} variant="outlined" onClick={() => open('group', launcher.payload ?? launcher)}>
|
|
45
|
-
{launcher.label || t('MessagingLaunchers.OPEN_GROUP')}
|
|
46
|
-
</Button>
|
|
47
|
-
))}
|
|
48
|
-
{error && <Alert severity="error">{error}</Alert>}
|
|
49
|
-
</Stack>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
2
|
-
import { Archive, Campaign, Groups, MoreVert, NotificationsOff, Person } from '@mui/icons-material';
|
|
3
|
-
import { Badge, Box, Button, CircularProgress, IconButton, List, ListItemButton, ListItemText, Menu, MenuItem, Stack, Typography } from '@mui/material';
|
|
4
|
-
import { useTranslation } from 'react-i18next';
|
|
5
|
-
|
|
6
|
-
import { useMessaging } from './MessagingProvider';
|
|
7
|
-
import { ConversationLaunchers } from './ConversationLaunchers';
|
|
8
|
-
|
|
9
|
-
// external_key (dcm 2.38.0+) is the consuming app's own identity for a
|
|
10
|
-
// managed conversation — ucm must not know what any particular app's values
|
|
11
|
-
// mean, so a label for it is host-supplied only; resolveManagedLabel receives
|
|
12
|
-
// the raw conversation (including external_key) and owns all interpretation.
|
|
13
|
-
// dcm's `other_user_id` (added alongside this) is a bare id, deliberately never
|
|
14
|
-
// a resolved name — the same reason a message's `sender_id` carries no display
|
|
15
|
-
// name — so a direct conversation's counterpart name is host-supplied, exactly
|
|
16
|
-
// like a managed conversation's label above.
|
|
17
|
-
function titleOf(conversation, t, resolveManagedLabel, resolveDirectUserName) {
|
|
18
|
-
// Not gated on kind === 'managed': a host resolver already returns null to
|
|
19
|
-
// mean "I have no label for this conversation" (the delegation contract
|
|
20
|
-
// this comment block describes above), so gating here duplicated a
|
|
21
|
-
// decision that belongs to the host and silently made the resolver
|
|
22
|
-
// unreachable for any kind the host later decided to label (found live:
|
|
23
|
-
// jg-ferien's resolver was taught to handle 'broadcast' too, but never got
|
|
24
|
-
// a chance to run because this gate excluded it).
|
|
25
|
-
if (resolveManagedLabel) {
|
|
26
|
-
const label = resolveManagedLabel(conversation);
|
|
27
|
-
if (label) return label;
|
|
28
|
-
}
|
|
29
|
-
if (conversation.kind === 'direct' && conversation.other_user_id != null && resolveDirectUserName) {
|
|
30
|
-
const name = resolveDirectUserName(conversation.other_user_id);
|
|
31
|
-
if (name) return name;
|
|
32
|
-
}
|
|
33
|
-
return conversation.title || conversation.other_user?.display_name || conversation.other_user?.username || t('MessagingList.UNTITLED');
|
|
34
|
-
}
|
|
35
|
-
function ordered(conversations) {
|
|
36
|
-
return [...conversations].sort((left, right) => new Date(right.last_message_at || 0) - new Date(left.last_message_at || 0));
|
|
37
|
-
}
|
|
38
|
-
function relativeTime(value, locale) {
|
|
39
|
-
if (!value) return null;
|
|
40
|
-
const seconds = Math.max(0, Math.floor((Date.now() - new Date(value).getTime()) / 1000));
|
|
41
|
-
const [amount, unit] = seconds < 60 ? [0, 'second'] : seconds < 3600 ? [Math.floor(seconds / 60), 'minute'] : seconds < 86400 ? [Math.floor(seconds / 3600), 'hour'] : [Math.floor(seconds / 86400), 'day'];
|
|
42
|
-
return new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'narrow' }).format(-amount, unit);
|
|
43
|
-
}
|
|
44
|
-
// jg's ConversationList renders a distinct icon per conversation kind so a
|
|
45
|
-
// user can identify type/person at a glance (getConversationAvatar); that
|
|
46
|
-
// glanceable identification is a feature, not layout, so it's reproduced
|
|
47
|
-
// here rather than left as a generic archive-status glyph.
|
|
48
|
-
function KindIcon({ conversation }) {
|
|
49
|
-
if (conversation.kind === 'broadcast') return <Campaign fontSize="small" />;
|
|
50
|
-
if (conversation.kind === 'group' || conversation.kind === 'managed' || conversation.kind === 'object_thread') return <Groups fontSize="small" />;
|
|
51
|
-
return <Person fontSize="small" />;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** A standalone, provider-backed list; scope picker metadata is supplied as launcher props. */
|
|
55
|
-
export function ConversationList({ onOpen, groupLaunchers, broadcastLauncher, autoOpenBroadcast = false, includeArchived = false, resolveManagedLabel, resolveDirectUserName }) {
|
|
56
|
-
const { t, i18n } = useTranslation();
|
|
57
|
-
const { cache, activeConversationId, loadMoreConversations, setConversationArchived, setConversationPreferences } = useMessaging();
|
|
58
|
-
const [menuConversation, setMenuConversation] = useState(null);
|
|
59
|
-
const [menuAnchor, setMenuAnchor] = useState(null);
|
|
60
|
-
const [loadingMore, setLoadingMore] = useState(false);
|
|
61
|
-
const [error, setError] = useState(null);
|
|
62
|
-
const conversations = useMemo(() => ordered(Object.values(cache.conversations).filter((item) => includeArchived || !item.archived_at && !item.archived)), [cache.conversations, includeArchived]);
|
|
63
|
-
const unreadFor = (conversation) => conversation.unread_count ?? cache.unread.by_conversation?.[conversation.id] ?? 0;
|
|
64
|
-
// Once opened, the broadcast conversation becomes a normal, recency-sorted
|
|
65
|
-
// row with real unread/preview state — hide the static launcher so it isn't
|
|
66
|
-
// shown twice (button + list row) with the button's stale, unread-less copy.
|
|
67
|
-
const broadcastAlreadyOpened = useMemo(() => Object.values(cache.conversations).some((item) => item.kind === 'broadcast'), [cache.conversations]);
|
|
68
|
-
|
|
69
|
-
const loadMore = async () => {
|
|
70
|
-
setLoadingMore(true); setError(null);
|
|
71
|
-
try { await loadMoreConversations(); } catch { setError(t('MessagingList.LOAD_ERROR')); } finally { setLoadingMore(false); }
|
|
72
|
-
};
|
|
73
|
-
const closeMenu = () => { setMenuConversation(null); setMenuAnchor(null); };
|
|
74
|
-
const archive = async () => {
|
|
75
|
-
try { await setConversationArchived(menuConversation.id, true); } catch { setError(t('MessagingList.ARCHIVE_ERROR')); }
|
|
76
|
-
closeMenu();
|
|
77
|
-
};
|
|
78
|
-
const toggleMute = async () => {
|
|
79
|
-
try { await setConversationPreferences(menuConversation.id, { muted: !menuConversation.muted }); } catch { setError(t('MessagingList.PREFERENCES_ERROR')); }
|
|
80
|
-
closeMenu();
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Stack spacing={1}>
|
|
85
|
-
<ConversationLaunchers groupLaunchers={groupLaunchers} broadcastLauncher={broadcastAlreadyOpened ? null : broadcastLauncher} autoOpenBroadcast={autoOpenBroadcast && !broadcastAlreadyOpened} onOpen={onOpen} />
|
|
86
|
-
{error && <Typography color="error" role="alert">{error}</Typography>}
|
|
87
|
-
{!conversations.length ? <Box sx={{ py: 3, textAlign: 'center' }}><Typography color="text.secondary">{t('MessagingList.EMPTY')}</Typography></Box> : (
|
|
88
|
-
<List disablePadding aria-label={t('MessagingList.LABEL')}>
|
|
89
|
-
{conversations.map((conversation) => {
|
|
90
|
-
const unread = unreadFor(conversation);
|
|
91
|
-
const archived = Boolean(conversation.archived || conversation.archived_at);
|
|
92
|
-
return <ListItemButton key={conversation.id} selected={conversation.id === activeConversationId} onClick={() => onOpen?.(conversation)} sx={{ minHeight: 64, opacity: archived ? 0.6 : 1 }}>
|
|
93
|
-
<Badge color="error" badgeContent={unread || null} max={99} sx={{ mr: 2 }}>
|
|
94
|
-
{archived ? <Archive fontSize="small" color="disabled" /> : <KindIcon conversation={conversation} />}
|
|
95
|
-
</Badge>
|
|
96
|
-
<ListItemText primary={<Stack direction="row" spacing={1} justifyContent="space-between"><Typography component="span" fontWeight={unread ? 700 : 400} noWrap>{titleOf(conversation, t, resolveManagedLabel, resolveDirectUserName)}</Typography>{relativeTime(conversation.last_message_at, i18n?.language) && <Typography component="span" variant="caption" color="text.secondary" noWrap>{relativeTime(conversation.last_message_at, i18n?.language)}</Typography>}</Stack>} secondary={conversation.last_message?.excerpt || t('MessagingList.NO_MESSAGES')} secondaryTypographyProps={{ noWrap: true }} />
|
|
97
|
-
<IconButton aria-label={t('MessagingList.ACTIONS')} onClick={(event) => { event.stopPropagation(); setMenuAnchor(event.currentTarget); setMenuConversation(conversation); }}><MoreVert /></IconButton>
|
|
98
|
-
</ListItemButton>;
|
|
99
|
-
})}
|
|
100
|
-
</List>
|
|
101
|
-
)}
|
|
102
|
-
{cache.cursors.conversations && <Button onClick={loadMore} disabled={loadingMore}>{loadingMore ? <CircularProgress size={18} /> : t('MessagingList.LOAD_MORE')}</Button>}
|
|
103
|
-
<Menu anchorEl={menuAnchor} open={Boolean(menuConversation)} onClose={closeMenu}>
|
|
104
|
-
<MenuItem onClick={toggleMute}><NotificationsOff fontSize="small" sx={{ mr: 1 }} />{menuConversation?.muted ? t('MessagingList.UNMUTE') : t('MessagingList.MUTE')}</MenuItem>
|
|
105
|
-
<MenuItem onClick={archive}><Archive fontSize="small" sx={{ mr: 1 }} />{t('MessagingList.ARCHIVE')}</MenuItem>
|
|
106
|
-
</Menu>
|
|
107
|
-
</Stack>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { useId, useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Alert,
|
|
4
|
-
Autocomplete,
|
|
5
|
-
Button,
|
|
6
|
-
CircularProgress,
|
|
7
|
-
Dialog,
|
|
8
|
-
DialogActions,
|
|
9
|
-
DialogContent,
|
|
10
|
-
DialogTitle,
|
|
11
|
-
TextField,
|
|
12
|
-
Typography,
|
|
13
|
-
useMediaQuery,
|
|
14
|
-
} from '@mui/material';
|
|
15
|
-
import { useTheme } from '@mui/material/styles';
|
|
16
|
-
import { useTranslation } from 'react-i18next';
|
|
17
|
-
|
|
18
|
-
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
19
|
-
|
|
20
|
-
export const DIRECT_MESSAGE_LAUNCHER_ALERT_SX = { mb: 1 };
|
|
21
|
-
|
|
22
|
-
function candidateLabel(candidate) {
|
|
23
|
-
if (!candidate) return '';
|
|
24
|
-
return candidate.display_name || candidate.name || candidate.label || String(candidate.id);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Starts direct conversations from host-provided people. Candidate discovery is
|
|
29
|
-
* intentionally outside messaging because memberships are host-domain data.
|
|
30
|
-
*/
|
|
31
|
-
export function DirectMessageLauncher({ candidates = [], scope, onOpen }) {
|
|
32
|
-
const { t } = useTranslation();
|
|
33
|
-
const { openDirectConversation } = useMessaging();
|
|
34
|
-
const theme = useTheme();
|
|
35
|
-
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
|
36
|
-
const [open, setOpen] = useState(false);
|
|
37
|
-
const [selectedId, setSelectedId] = useState(null);
|
|
38
|
-
const [starting, setStarting] = useState(false);
|
|
39
|
-
const [error, setError] = useState(null);
|
|
40
|
-
const titleId = useId();
|
|
41
|
-
const selectedCandidate = useMemo(
|
|
42
|
-
() => candidates.find((candidate) => candidate.id === selectedId) || null,
|
|
43
|
-
[candidates, selectedId],
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const showPicker = () => {
|
|
47
|
-
setSelectedId(null);
|
|
48
|
-
setError(null);
|
|
49
|
-
setOpen(true);
|
|
50
|
-
};
|
|
51
|
-
const closePicker = () => {
|
|
52
|
-
if (!starting) setOpen(false);
|
|
53
|
-
};
|
|
54
|
-
const start = async () => {
|
|
55
|
-
if (selectedId == null || starting) return;
|
|
56
|
-
setStarting(true);
|
|
57
|
-
setError(null);
|
|
58
|
-
try {
|
|
59
|
-
const effectiveScope = selectedCandidate?.scope ?? scope;
|
|
60
|
-
const conversation = await openDirectConversation({
|
|
61
|
-
target_user_id: selectedId,
|
|
62
|
-
...(effectiveScope == null ? {} : { scope: effectiveScope }),
|
|
63
|
-
});
|
|
64
|
-
setOpen(false);
|
|
65
|
-
onOpen?.(conversation);
|
|
66
|
-
} catch (requestError) {
|
|
67
|
-
setError(extractApiErrorMessage(requestError) || t('MessagingDirect.START_ERROR'));
|
|
68
|
-
} finally {
|
|
69
|
-
setStarting(false);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<>
|
|
75
|
-
<Button variant="outlined" onClick={showPicker}>{t('MessagingDirect.LAUNCH')}</Button>
|
|
76
|
-
<Dialog open={open} onClose={closePicker} fullScreen={fullScreen} aria-labelledby={titleId}>
|
|
77
|
-
<DialogTitle id={titleId}>{t('MessagingDirect.TITLE')}</DialogTitle>
|
|
78
|
-
<DialogContent>
|
|
79
|
-
{error && <Alert severity="error" sx={DIRECT_MESSAGE_LAUNCHER_ALERT_SX}>{error}</Alert>}
|
|
80
|
-
{starting && <Typography role="status" sx={{ mb: 1 }}><CircularProgress size={16} sx={{ mr: 1 }} />{t('MessagingDirect.STARTING')}</Typography>}
|
|
81
|
-
{candidates.length === 0 ? (
|
|
82
|
-
<Typography>{t('MessagingDirect.EMPTY')}</Typography>
|
|
83
|
-
) : (
|
|
84
|
-
<Autocomplete
|
|
85
|
-
fullWidth
|
|
86
|
-
options={candidates}
|
|
87
|
-
value={selectedCandidate}
|
|
88
|
-
onChange={(_event, nextValue) => setSelectedId(nextValue?.id ?? null)}
|
|
89
|
-
getOptionLabel={candidateLabel}
|
|
90
|
-
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
91
|
-
disabled={starting}
|
|
92
|
-
noOptionsText={t('MessagingDirect.NO_MATCHES')}
|
|
93
|
-
renderInput={(params) => (
|
|
94
|
-
<TextField
|
|
95
|
-
{...params}
|
|
96
|
-
label={t('MessagingDirect.CANDIDATES')}
|
|
97
|
-
placeholder={t('MessagingDirect.SEARCH_PLACEHOLDER')}
|
|
98
|
-
/>
|
|
99
|
-
)}
|
|
100
|
-
/>
|
|
101
|
-
)}
|
|
102
|
-
</DialogContent>
|
|
103
|
-
<DialogActions>
|
|
104
|
-
<Button onClick={closePicker} disabled={starting}>{t('MessagingDirect.CANCEL')}</Button>
|
|
105
|
-
<Button variant="contained" onClick={start} disabled={selectedId == null || starting}>
|
|
106
|
-
{t('MessagingDirect.START')}
|
|
107
|
-
</Button>
|
|
108
|
-
</DialogActions>
|
|
109
|
-
</Dialog>
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
112
|
-
}
|