Skip to content

Commit 9f57f21

Browse files
authored
chore(demo): allow to build notifications with multiple actions (#3190)
1 parent 32dc9fa commit 9f57f21

5 files changed

Lines changed: 250 additions & 58 deletions

File tree

examples/vite/src/App.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ if (!apiKey) {
8989
throw new Error('VITE_STREAM_API_KEY is not defined');
9090
}
9191

92-
const options: ChannelOptions = {
93-
presence: true,
94-
state: true,
95-
limit: 10,
96-
};
97-
9892
const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
9993

10094
// @ts-ignore
@@ -211,6 +205,18 @@ const App = () => {
211205
() => new URLSearchParams(window.location.search),
212206
[],
213207
);
208+
const options = useMemo<ChannelOptions>(
209+
() => ({
210+
// filter_values: {
211+
// user_id: userId,
212+
// },
213+
// predefined_filter: 'livestreams_channels',
214+
presence: true,
215+
state: true,
216+
limit: 10,
217+
}),
218+
[],
219+
);
214220
const initialChannelId = useMemo(() => getSelectedChannelIdFromUrl(), []);
215221
const initialChatView = useMemo(() => getSelectedChatViewFromUrl(), []);
216222
const initialThreadId = useMemo(

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

Lines changed: 170 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
IconArrowLeft,
77
IconChevronRight,
88
Button,
9+
IconMinus,
910
IconRefresh,
1011
IconArrowUp,
1112
IconCheckmark,
@@ -33,6 +34,7 @@ import {
3334
parseDuration,
3435
severityOptions,
3536
targetPanelOptions,
37+
type NotificationDraftAction,
3638
type NotificationDraft,
3739
type QueuedNotification,
3840
} from './triggerNotificationUtils';
@@ -66,6 +68,12 @@ const formatDurationLabel = (duration: number) => {
6668
return `${(duration / 1000).toFixed(1)}s`;
6769
};
6870

71+
const formatActionsLabel = (count: number) => `${count} action${count === 1 ? '' : 's'}`;
72+
73+
const isDraftActionReady = (
74+
action: Pick<NotificationDraftAction, 'feedback' | 'label'>,
75+
) => action.label.trim().length > 0 && action.feedback.trim().length > 0;
76+
6977
const NotificationEntrySelect = ({
7078
label,
7179
onChange,
@@ -154,6 +162,11 @@ const NotificationChipList = ({
154162
<span className='app__notification-dialog__chip-panel'>
155163
{notification.targetPanel}
156164
</span>
165+
{notification.actions.length > 0 && (
166+
<span className='app__notification-dialog__chip-panel'>
167+
{formatActionsLabel(notification.actions.length)}
168+
</span>
169+
)}
157170
</span>
158171
<Button
159172
appearance='ghost'
@@ -180,21 +193,32 @@ const NotificationChipList = ({
180193
};
181194

182195
const NotificationDraftForm = ({
196+
addDraftAction,
183197
draft,
184198
queueCurrentDraft,
185199
queuedNotifications,
186200
registerQueuedNotifications,
201+
toggleDraftActionInPayload,
187202
removeQueuedNotification,
188203
setDraft,
204+
updateDraftAction,
189205
}: {
206+
addDraftAction: () => void;
190207
draft: NotificationDraft;
191208
queueCurrentDraft: () => void;
192209
queuedNotifications: QueuedNotification[];
193210
registerQueuedNotifications: () => void;
211+
toggleDraftActionInPayload: (id: string) => void;
194212
removeQueuedNotification: (id: string) => void;
195213
setDraft: Dispatch<SetStateAction<NotificationDraft>>;
214+
updateDraftAction: <Key extends keyof NotificationDraftAction>(
215+
actionId: string,
216+
key: Key,
217+
value: NotificationDraftAction[Key],
218+
) => void;
196219
}) => {
197220
const canQueueCurrentDraft = isDraftReady(draft);
221+
const canAddDraftAction = draft.actions.every((action) => action.includedInPayload);
198222

199223
return (
200224
<>
@@ -254,27 +278,68 @@ const NotificationDraftForm = ({
254278
options={targetPanelOptions}
255279
value={draft.targetPanel}
256280
/>
257-
<TextInput
258-
className='app__notification-dialog__text-input'
259-
label='Action Label (optional)'
260-
onChange={(event) =>
261-
setDraft((current) => ({ ...current, actionLabel: event.target.value }))
262-
}
263-
placeholder='Optional button label'
264-
value={draft.actionLabel}
265-
/>
266-
<TextInput
267-
className='app__notification-dialog__text-input app__notification-dialog__text-input--wide'
268-
label='Action Follow-up Message (optional)'
269-
onChange={(event) =>
270-
setDraft((current) => ({
271-
...current,
272-
actionFeedback: event.target.value,
273-
}))
274-
}
275-
placeholder='Optional notification triggered by the action button'
276-
value={draft.actionFeedback}
277-
/>
281+
<div className='app__notification-dialog__actions'>
282+
<div className='app__notification-dialog__actions-header'>
283+
<span className='app__notification-dialog__field-label'>
284+
Action(s) (optional)
285+
</span>
286+
</div>
287+
<div className='app__notification-dialog__actions-list'>
288+
{draft.actions.map((action, index) => {
289+
const canAddActionToPayload = isDraftActionReady(action);
290+
291+
return (
292+
<div className='app__notification-dialog__action-row' key={action.id}>
293+
<TextInput
294+
className='app__notification-dialog__text-input'
295+
label={`Action ${index + 1} Label`}
296+
onChange={(event) =>
297+
updateDraftAction(action.id, 'label', event.target.value)
298+
}
299+
placeholder='Optional button label'
300+
value={action.label}
301+
/>
302+
<TextInput
303+
className='app__notification-dialog__text-input'
304+
label={`Action ${index + 1} Follow-up Message`}
305+
onChange={(event) =>
306+
updateDraftAction(action.id, 'feedback', event.target.value)
307+
}
308+
placeholder='Optional alert triggered by the action button'
309+
value={action.feedback}
310+
/>
311+
<Button
312+
aria-label={
313+
action.includedInPayload
314+
? 'Remove action from payload'
315+
: 'Add action to payload'
316+
}
317+
appearance='outline'
318+
circular
319+
className='app__notification-dialog__action-toggle'
320+
disabled={!action.includedInPayload && !canAddActionToPayload}
321+
onClick={() => toggleDraftActionInPayload(action.id)}
322+
size='xs'
323+
variant='secondary'
324+
>
325+
{action.includedInPayload ? <IconMinus /> : <IconPlusSmall />}
326+
</Button>
327+
</div>
328+
);
329+
})}
330+
</div>
331+
<Button
332+
appearance='ghost'
333+
className='app__notification-dialog__action-add'
334+
disabled={!canAddDraftAction}
335+
onClick={addDraftAction}
336+
size='sm'
337+
variant='secondary'
338+
>
339+
<IconPlusSmall />
340+
Add action
341+
</Button>
342+
</div>
278343
</div>
279344
</div>
280345
</Prompt.Body>
@@ -325,7 +390,25 @@ export const NotificationPromptDialog = ({
325390
}: {
326391
referenceElement: HTMLElement | null;
327392
}) => {
328-
const [draft, setDraft] = useState(initialDraft);
393+
const actionIdRef = useRef(0);
394+
const createDraftAction = useCallback((): NotificationDraftAction => {
395+
actionIdRef.current += 1;
396+
397+
return {
398+
feedback: '',
399+
id: `draft-notification-action-${actionIdRef.current}`,
400+
includedInPayload: false,
401+
label: '',
402+
};
403+
}, []);
404+
const createInitialDraftState = useCallback(
405+
(): NotificationDraft => ({
406+
...initialDraft,
407+
actions: [createDraftAction()],
408+
}),
409+
[createDraftAction],
410+
);
411+
const [draft, setDraft] = useState<NotificationDraft>(() => createInitialDraftState());
329412
const [queuedNotifications, setQueuedNotifications] = useState<QueuedNotification[]>(
330413
[],
331414
);
@@ -337,9 +420,9 @@ export const NotificationPromptDialog = ({
337420
const dialogIsOpen = useDialogIsOpen(notificationPromptDialogId, dialogManager?.id);
338421

339422
const resetState = useCallback(() => {
340-
setDraft(initialDraft);
423+
setDraft(createInitialDraftState());
341424
setQueuedNotifications([]);
342-
}, []);
425+
}, [createInitialDraftState]);
343426

344427
useEffect(() => {
345428
if (dialogIsOpen) return;
@@ -376,8 +459,14 @@ export const NotificationPromptDialog = ({
376459
setQueuedNotifications((current) => [
377460
...current,
378461
{
379-
actionFeedback: draft.actionFeedback,
380-
actionLabel: draft.actionLabel,
462+
actions: draft.actions
463+
.map((action) => ({
464+
includedInPayload: action.includedInPayload,
465+
feedback: action.feedback.trim(),
466+
label: action.label.trim(),
467+
}))
468+
.filter((action) => action.includedInPayload && action.label && action.feedback)
469+
.map(({ feedback, label }) => ({ feedback, label })),
381470
duration,
382471
entryDirection: draft.entryDirection as NotificationListEnterFrom,
383472
id: `queued-notification-${chipIdRef.current}`,
@@ -386,8 +475,8 @@ export const NotificationPromptDialog = ({
386475
targetPanel: draft.targetPanel as NotificationTargetPanel,
387476
},
388477
]);
389-
setDraft(initialDraft);
390-
}, [draft]);
478+
setDraft(createInitialDraftState());
479+
}, [createInitialDraftState, draft]);
391480

392481
const registerQueuedNotifications = useCallback(() => {
393482
queuedNotifications.forEach(publishNotification);
@@ -398,6 +487,56 @@ export const NotificationPromptDialog = ({
398487
setQueuedNotifications((current) => current.filter((item) => item.id !== id));
399488
}, []);
400489

490+
const addDraftAction = useCallback(() => {
491+
setDraft((current) => ({
492+
...current,
493+
actions: [...current.actions, createDraftAction()],
494+
}));
495+
}, [createDraftAction]);
496+
497+
const toggleDraftActionInPayload = useCallback((id: string) => {
498+
setDraft((current) => {
499+
return {
500+
...current,
501+
actions: current.actions.map((action) =>
502+
action.id === id
503+
? { ...action, includedInPayload: !action.includedInPayload }
504+
: action,
505+
),
506+
};
507+
});
508+
}, []);
509+
510+
const updateDraftAction = useCallback(
511+
<Key extends keyof NotificationDraftAction>(
512+
actionId: string,
513+
key: Key,
514+
value: NotificationDraftAction[Key],
515+
) => {
516+
setDraft((current) => ({
517+
...current,
518+
actions: current.actions.map((action) =>
519+
action.id === actionId
520+
? {
521+
...action,
522+
[key]: value,
523+
includedInPayload:
524+
key === 'feedback' || key === 'label'
525+
? isDraftActionReady({
526+
...action,
527+
[key]: value,
528+
})
529+
? action.includedInPayload
530+
: false
531+
: action.includedInPayload,
532+
}
533+
: action,
534+
),
535+
}));
536+
},
537+
[],
538+
);
539+
401540
return (
402541
<DraggableDialog
403542
dialogClassName='app__notification-dialog'
@@ -412,12 +551,15 @@ export const NotificationPromptDialog = ({
412551
title='Trigger Notification'
413552
>
414553
<NotificationDraftForm
554+
addDraftAction={addDraftAction}
415555
draft={draft}
416556
queueCurrentDraft={queueCurrentDraft}
417557
queuedNotifications={queuedNotifications}
418558
registerQueuedNotifications={registerQueuedNotifications}
419559
removeQueuedNotification={removeQueuedNotification}
420560
setDraft={setDraft}
561+
toggleDraftActionInPayload={toggleDraftActionInPayload}
562+
updateDraftAction={updateDraftAction}
421563
/>
422564
</DraggableDialog>
423565
);

0 commit comments

Comments
 (0)