Skip to content

Commit 2060768

Browse files
authored
feat: add notification API wrapper (#3096)
BREAKING CHANGE: - Removed legacy notification text callback props from Message props: getDeleteMessageErrorNotification, getFetchReactionsErrorNotification, getFlagMessageErrorNotification, getFlagMessageSuccessNotification, getMarkMessageUnreadErrorNotification, getMarkMessageUnreadSuccessNotification, getMuteUserErrorNotification, getMuteUserSuccessNotification, getPinMessageErrorNotification. Notification customization must now be done through notification translators (Streami18n translationBuilder topic: "notification") and/or custom MessageActions. BREAKING CHANGE: - Message action handlers no longer publish notifications internally. Errors now propagate to call sites, which are responsible for success/error notifications. BREAKING CHANGE: - Removed ConnectionStatus component. Connection notification UI is app responsibility now.
1 parent 8b13863 commit 2060768

76 files changed

Lines changed: 2291 additions & 1266 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/vite/src/App.tsx

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
Chat,
2121
ChatView,
2222
createIcon,
23+
DialogManagerProvider,
2324
MessageReactions,
2425
type NotificationListProps,
2526
NotificationList,
@@ -52,6 +53,7 @@ import {
5253
getSelectedChatViewFromUrl,
5354
} from './ChatLayout/Sync.tsx';
5455
import { LoadingScreen } from './LoadingScreen/LoadingScreen.tsx';
56+
import { SystemNotification } from './SystemNotification/SystemNotification.tsx';
5557
import { chatViewSelectorItemSet } from './Sidebar/ChatViewSelectorItemSet.tsx';
5658
import {
5759
CustomAttachmentActions,
@@ -206,6 +208,7 @@ const MessageUiOverride = messageUiVariant
206208
const systemMessageVariant = getSystemMessageVariant();
207209
const reactionsVariant = getReactionsVariant();
208210
const attachmentActionsVariant = getAttachmentActionsVariant();
211+
const globalDialogManager = 'globalDialogManager';
209212

210213
const CustomAttachmentWithActions = (props: AttachmentProps) => (
211214
<Attachment {...props} AttachmentActions={CustomAttachmentActions} />
@@ -411,27 +414,32 @@ const App = () => {
411414
style={initialAppLayoutStyle}
412415
data-variant={messageUiVariant ?? undefined}
413416
>
414-
<PanelLayoutStyleSync layoutRef={appLayoutRef} />
415-
<ChatViewSelectorWidthSync
416-
iconOnly={chatView.iconOnly}
417-
layoutRef={appLayoutRef}
418-
/>
419-
<ChatView>
420-
<ChatStateSync initialChatView={initialChatView} />
421-
<SidebarLayoutSync />
422-
<ChannelsPanels
423-
filters={filters}
417+
<SystemNotification />
418+
<div className='app-chat-layout__body'>
419+
<PanelLayoutStyleSync layoutRef={appLayoutRef} />
420+
<ChatViewSelectorWidthSync
424421
iconOnly={chatView.iconOnly}
425-
initialChannelId={initialChannelId ?? undefined}
426-
itemSet={chatViewSelectorItemSet}
427-
options={options}
428-
sort={sort}
422+
layoutRef={appLayoutRef}
429423
/>
430-
<ThreadsPanels
431-
iconOnly={chatView.iconOnly}
432-
itemSet={chatViewSelectorItemSet}
433-
/>
434-
</ChatView>
424+
<ChatView>
425+
<DialogManagerProvider id={globalDialogManager}>
426+
<ChatStateSync initialChatView={initialChatView} />
427+
<SidebarLayoutSync />
428+
<ChannelsPanels
429+
filters={filters}
430+
iconOnly={chatView.iconOnly}
431+
initialChannelId={initialChannelId ?? undefined}
432+
itemSet={chatViewSelectorItemSet}
433+
options={options}
434+
sort={sort}
435+
/>
436+
<ThreadsPanels
437+
iconOnly={chatView.iconOnly}
438+
itemSet={chatViewSelectorItemSet}
439+
/>
440+
</DialogManagerProvider>
441+
</ChatView>
442+
</div>
435443
</div>
436444
</Chat>
437445
</SidebarProvider>

examples/vite/src/AppSettings/ActionsMenu/ActionsMenu.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { useMemo, useState } from 'react';
21
import type { ComponentProps } from 'react';
2+
import { useState } from 'react';
33
import {
44
Button,
55
ContextMenu,
66
ContextMenuButton,
7-
DialogManagerProvider,
87
IconBolt,
98
useContextMenuContext,
109
useDialogIsOpen,
1110
useDialogOnNearestManager,
12-
type ContextMenuItemComponent,
1311
} from 'stream-chat-react';
1412
import {
1513
NotificationPromptDialog,
@@ -57,23 +55,18 @@ const ActionsMenuButton = ({
5755
);
5856

5957
export const ActionsMenu = ({ iconOnly = true }: { iconOnly?: boolean }) => (
60-
<DialogManagerProvider id='app-actions-menu-dialog-manager'>
61-
<ActionsMenuInner iconOnly={iconOnly} />
62-
</DialogManagerProvider>
58+
<ActionsMenuInner iconOnly={iconOnly} />
6359
);
6460

65-
function TriggerNotification() {
61+
function TriggerNotificationAction({ onTrigger }: { onTrigger: () => void }) {
6662
const { closeMenu } = useContextMenuContext();
67-
const { dialog: notificationDialog } = useDialogOnNearestManager({
68-
id: notificationPromptDialogId,
69-
});
7063

7164
return (
7265
<ContextMenuButton
7366
label='Trigger Notification'
7467
onClick={() => {
7568
closeMenu();
76-
notificationDialog.open();
69+
onTrigger();
7770
}}
7871
/>
7972
);
@@ -86,6 +79,9 @@ const ActionsMenuInner = ({ iconOnly }: { iconOnly: boolean }) => {
8679
const { dialog: actionsMenuDialog, dialogManager } = useDialogOnNearestManager({
8780
id: actionsMenuDialogId,
8881
});
82+
const { dialog: notificationDialog } = useDialogOnNearestManager({
83+
id: notificationPromptDialogId,
84+
});
8985

9086
const menuIsOpen = useDialogIsOpen(actionsMenuDialogId, dialogManager?.id);
9187

@@ -108,7 +104,7 @@ const ActionsMenuInner = ({ iconOnly }: { iconOnly: boolean }) => {
108104
tabIndex={-1}
109105
trapFocus
110106
>
111-
<TriggerNotification />
107+
<TriggerNotificationAction onTrigger={notificationDialog.open} />
112108
</ContextMenu>
113109
<NotificationPromptDialog referenceElement={menuButtonElement} />
114110
</div>

examples/vite/src/AppSettings/ActionsMenu/NotificationPromptDialog.tsx

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
22
import type { Dispatch, PointerEvent as ReactPointerEvent, SetStateAction } from 'react';
33
import type { NotificationSeverity } from 'stream-chat';
44
import {
5-
addNotificationTargetTag,
65
IconArrowDown,
76
IconArrowLeft,
87
IconChevronRight,
@@ -19,7 +18,7 @@ import {
1918
NumericInput,
2019
Prompt,
2120
TextInput,
22-
useChatContext,
21+
useNotificationApi,
2322
useDialogIsOpen,
2423
useDialogOnNearestManager,
2524
type NotificationListEnterFrom,
@@ -197,16 +196,17 @@ const NotificationDraftForm = ({
197196
options={severityOptions}
198197
value={draft.severity}
199198
/>
200-
<label className='app__notification-dialog__field'>
199+
<div className='app__notification-dialog__field'>
201200
<span className='app__notification-dialog__field-label'>Duration (ms)</span>
202201
<NumericInput
202+
aria-label='Duration (ms)'
203203
min={0}
204204
onChange={(event) =>
205205
setDraft((current) => ({ ...current, duration: event.target.value }))
206206
}
207207
value={draft.duration}
208208
/>
209-
</label>
209+
</div>
210210
<NotificationEntrySelect
211211
label='Entry Direction'
212212
onChange={(value) =>
@@ -307,7 +307,7 @@ export const NotificationPromptDialog = ({
307307
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
308308
const chipIdRef = useRef(0);
309309
const shellRef = useRef<HTMLDivElement | null>(null);
310-
const { client } = useChatContext();
310+
const { addNotification } = useNotificationApi();
311311
const { dialog, dialogManager } = useDialogOnNearestManager({
312312
id: notificationPromptDialogId,
313313
});
@@ -362,26 +362,22 @@ export const NotificationPromptDialog = ({
362362
dialog.close();
363363
}, [dialog]);
364364

365-
const addNotification = useCallback(
365+
const publishNotification = useCallback(
366366
(notification: QueuedNotification) => {
367-
client.notifications.add({
368-
message: notification.message,
369-
origin: {
370-
context: {
371-
entryDirection: notification.entryDirection,
372-
panel: notification.targetPanel,
373-
},
374-
emitter: 'vite-preview/ActionsMenu',
375-
},
376-
options: {
377-
actions: buildNotificationActions(notification),
378-
duration: notification.duration,
379-
severity: notification.severity,
380-
tags: addNotificationTargetTag(notification.targetPanel),
367+
addNotification({
368+
actions: buildNotificationActions(notification),
369+
context: {
370+
entryDirection: notification.entryDirection,
371+
panel: notification.targetPanel,
381372
},
373+
duration: notification.duration,
374+
emitter: 'vite-preview/ActionsMenu',
375+
message: notification.message,
376+
severity: notification.severity,
377+
targetPanels: [notification.targetPanel],
382378
});
383379
},
384-
[client],
380+
[addNotification],
385381
);
386382

387383
const queueCurrentDraft = useCallback(() => {
@@ -406,9 +402,9 @@ export const NotificationPromptDialog = ({
406402
}, [draft]);
407403

408404
const registerQueuedNotifications = useCallback(() => {
409-
queuedNotifications.forEach(addNotification);
405+
queuedNotifications.forEach(publishNotification);
410406
closeDialog();
411-
}, [addNotification, closeDialog, queuedNotifications]);
407+
}, [closeDialog, publishNotification, queuedNotifications]);
412408

413409
const removeQueuedNotification = useCallback((id: string) => {
414410
setQueuedNotifications((current) => current.filter((item) => item.id !== id));

examples/vite/src/AppSettings/AppSettings.scss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
}
2121
}
2222

23-
.app__actions-menu {
23+
.str-chat__dialog-contents.app__actions-menu,
24+
.str-chat__context-menu.app__actions-menu {
2425
min-width: min(320px, calc(100vw - 32px));
2526
max-width: min(320px, calc(100vw - 32px));
2627
}
@@ -249,8 +250,7 @@
249250
display: flex;
250251
flex-direction: column;
251252
width: min(920px, 90vw);
252-
max-height: min(80vh, 760px);
253-
min-height: min(520px, 72vh);
253+
height: min(80vh, 760px);
254254
background: var(--background-core-elevation-2);
255255
color: var(--text-primary);
256256
border: 1px solid var(--border-core-default);
@@ -282,7 +282,7 @@
282282
.app__settings-modal__tabs {
283283
overflow-y: auto;
284284
overscroll-behavior: contain;
285-
border-right: 1px solid var(--border-core-default);
285+
border-inline-end: 1px solid var(--border-core-default);
286286
padding: 10px;
287287
}
288288

@@ -383,7 +383,7 @@
383383
}
384384

385385
.app__settings-modal__tabs {
386-
border-right: 1px solid var(--border-core-default);
386+
border-inline-end: 1px solid var(--border-core-default);
387387
border-bottom: 0;
388388
display: block;
389389
gap: 0;

examples/vite/src/LoadingScreen/LoadingScreen.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ export const LoadingScreen = ({
5757
</div>
5858
</div>
5959
<div className='str-chat__channel'>
60-
<div className='str-chat__main-panel'>
61-
<div className='str-chat__main-panel-inner'>
62-
<div className='str-chat__window app-loading-screen__window'>
63-
<LoadingChannel />
60+
<div className='str-chat__container'>
61+
<div className='str-chat__main-panel'>
62+
<div className='str-chat__main-panel-inner'>
63+
<div className='str-chat__window app-loading-screen__window'>
64+
<LoadingChannel />
65+
</div>
6466
</div>
6567
</div>
6668
</div>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
.str-chat__system-notification {
2+
display: flex;
3+
flex-shrink: 0;
4+
align-items: center;
5+
justify-content: center;
6+
gap: var(--spacing-xs, 8px);
7+
padding-block: var(--spacing-xs, 8px);
8+
padding-inline: var(--spacing-sm, 12px);
9+
background: var(--background-core-surface-default, #ebeef1);
10+
color: var(--chat-text-system, #414552);
11+
font-feature-settings:
12+
'liga' off,
13+
'clig' off;
14+
font-size: var(--typography-font-size-xs, 12px);
15+
font-style: normal;
16+
font-weight: var(--typography-font-weight-semi-bold, 600);
17+
line-height: var(--typography-line-height-tight, 16px);
18+
animation: str-chat__system-notification-slide-in 300ms ease-out both;
19+
overflow: hidden;
20+
width: 100%;
21+
z-index: 2;
22+
}
23+
24+
.str-chat__system-notification--exiting {
25+
animation: str-chat__system-notification-slide-out 300ms ease-in both;
26+
}
27+
28+
.str-chat__system-notification--interactive {
29+
cursor: pointer;
30+
}
31+
32+
.str-chat__system-notification-icon {
33+
align-items: center;
34+
display: inline-flex;
35+
flex-shrink: 0;
36+
}
37+
38+
.str-chat__system-notification-message {
39+
white-space: nowrap;
40+
overflow-y: visible;
41+
overflow-x: hidden;
42+
overflow-x: clip;
43+
text-overflow: ellipsis;
44+
}
45+
46+
.str-chat__system-notification--loading .str-chat__system-notification-icon {
47+
animation: str-chat__system-notification-spin 1.5s linear infinite;
48+
}
49+
50+
@keyframes str-chat__system-notification-slide-in {
51+
from {
52+
max-height: 0;
53+
opacity: 0;
54+
padding-block: 0;
55+
}
56+
57+
to {
58+
max-height: 4rem;
59+
opacity: 1;
60+
}
61+
}
62+
63+
@keyframes str-chat__system-notification-slide-out {
64+
from {
65+
max-height: 4rem;
66+
opacity: 1;
67+
}
68+
69+
to {
70+
max-height: 0;
71+
opacity: 0;
72+
padding-block: 0;
73+
}
74+
}
75+
76+
@keyframes str-chat__system-notification-spin {
77+
from {
78+
transform: rotate(0deg);
79+
}
80+
81+
to {
82+
transform: rotate(360deg);
83+
}
84+
}

0 commit comments

Comments
 (0)