Skip to content

Commit 67f8c85

Browse files
committed
fix(web): flatten message center list
1 parent d343775 commit 67f8c85

3 files changed

Lines changed: 18 additions & 85 deletions

File tree

apps/web/src/components/MessageCenter.module.css

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@
101101
}
102102

103103
.close.close:focus-visible,
104-
.filter:focus-visible,
105104
.markAll:focus-visible,
106105
.syncStatus button:focus-visible,
107106
.itemSummary:focus-visible,
@@ -116,24 +115,11 @@
116115
flex: 0 0 auto;
117116
display: flex;
118117
align-items: center;
119-
justify-content: space-between;
120-
gap: 10px;
121-
padding: 12px 14px;
118+
justify-content: flex-end;
119+
padding: 9px 14px;
122120
border-bottom: 1px solid var(--border-soft);
123121
}
124122

125-
.filters {
126-
display: inline-flex;
127-
align-items: center;
128-
gap: 4px;
129-
min-width: 0;
130-
padding: 3px;
131-
border: 1px solid var(--border);
132-
border-radius: var(--radius-pill, 999px);
133-
background: var(--bg-subtle);
134-
}
135-
136-
.filter,
137123
.markAll {
138124
border: 0;
139125
background: transparent;
@@ -143,39 +129,10 @@
143129
line-height: 1;
144130
}
145131

146-
.filter {
147-
min-height: 26px;
148-
display: inline-flex;
149-
align-items: center;
150-
gap: 5px;
151-
padding: 0 9px;
152-
border-radius: var(--radius-pill, 999px);
153-
}
154-
155-
.filter:hover,
156132
.markAll:hover:not(:disabled) {
157133
color: var(--text-strong);
158134
}
159135

160-
.filterActive {
161-
background: var(--bg-elevated, var(--bg-panel));
162-
color: var(--text-strong);
163-
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
164-
}
165-
166-
.filterBadge {
167-
min-width: 15px;
168-
height: 15px;
169-
padding: 0 4px;
170-
border-radius: 999px;
171-
background: var(--red);
172-
color: var(--accent-contrast, #fff);
173-
font-size: 9px;
174-
font-weight: 700;
175-
line-height: 15px;
176-
text-align: center;
177-
}
178-
179136
.markAll {
180137
flex: 0 0 auto;
181138
padding: 6px 4px;
@@ -485,23 +442,6 @@
485442
border-radius: var(--radius) var(--radius) 0 0;
486443
}
487444

488-
.controls {
489-
align-items: stretch;
490-
flex-direction: column;
491-
}
492-
493-
.filters {
494-
width: 100%;
495-
}
496-
497-
.filter {
498-
flex: 1;
499-
}
500-
501-
.markAll {
502-
align-self: flex-end;
503-
}
504-
505445
.footer {
506446
align-items: stretch;
507447
flex-direction: column;

apps/web/src/components/MessageCenter.tsx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Button } from '@open-design/components';
2-
import { useCallback, useEffect, useId, useMemo, useRef, useState, type RefObject } from 'react';
2+
import { useCallback, useEffect, useId, useRef, useState, type RefObject } from 'react';
33
import { createPortal } from 'react-dom';
44

55
import { useI18n, type Locale } from '../i18n';
@@ -11,19 +11,12 @@ import {
1111
pullMessageCenter,
1212
readAnonymousMessages,
1313
readAnonymousReadIds,
14-
type MessageCenterFilter,
1514
type MessageCenterMessage,
1615
writeAnonymousState,
1716
} from '../message-center-client';
1817
import { Icon } from './Icon';
1918
import styles from './MessageCenter.module.css';
2019

21-
const FILTERS: Array<{ id: MessageCenterFilter; label: 'messageCenter.filterAll' | 'messageCenter.filterUnread' | 'messageCenter.filterRead' }> = [
22-
{ id: 'all', label: 'messageCenter.filterAll' },
23-
{ id: 'unread', label: 'messageCenter.filterUnread' },
24-
{ id: 'read', label: 'messageCenter.filterRead' },
25-
];
26-
2720
function unreadBadgeLabel(count: number): string {
2821
return count > 9 ? '9+' : String(count);
2922
}
@@ -74,7 +67,6 @@ export function MessageCenter({
7467
},
7568
[onOpenChange],
7669
);
77-
const [filter, setFilter] = useState<MessageCenterFilter>('all');
7870
const [messages, setMessages] = useState<MessageCenterMessage[]>([]);
7971
const [readIds, setReadIds] = useState<Set<string>>(new Set());
8072
const [loggedIn, setLoggedIn] = useState(false);
@@ -174,10 +166,6 @@ export function MessageCenter({
174166
useEffect(() => {
175167
onUnreadCountChange?.(unreadCount);
176168
}, [unreadCount, onUnreadCountChange]);
177-
const visibleMessages = useMemo(
178-
() => messages.filter((message) => filter === 'all' || (filter === 'read' ? Boolean(message.readAt) : !message.readAt)),
179-
[filter, messages],
180-
);
181169

182170
/** The control keyboard focus must land on after the panel closes. Opening
183171
* focuses the portaled dialog, so closing always unmounts the focused node —
@@ -241,15 +229,14 @@ export function MessageCenter({
241229
};
242230

243231
const openLabel = unreadCount > 0 ? `${t('messageCenter.openAria')} (${t('messageCenter.unreadCount', { count: unreadCount })})` : t('messageCenter.openAria');
244-
const emptyTitle = filter === 'unread' ? t('messageCenter.emptyUnreadTitle') : filter === 'read' ? t('messageCenter.emptyReadTitle') : t('messageCenter.emptyAllTitle');
245232

246233
return <div className={styles.root}>
247234
{hideTrigger ? null : <button ref={triggerRef} type="button" className={`settings-icon-btn od-tooltip ${styles.trigger}`} onClick={() => setOpen(!open)} title={t('messageCenter.openAria')} data-tooltip={t('messageCenter.openAria')} data-tooltip-placement="bottom" aria-label={openLabel} aria-haspopup="dialog" aria-expanded={open} data-testid="message-center-trigger">
248235
<Icon name="bell" size={17} />{unreadCount > 0 ? <span className={styles.badge} aria-hidden>{unreadBadgeLabel(unreadCount)}</span> : null}
249236
</button>}
250237
{open ? createPortal(<div className={styles.backdrop} data-testid="message-center-backdrop"><aside ref={panelRef} className={styles.panel} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1} data-testid="message-center-dialog">
251238
<header className={styles.header}><div className={styles.headerCopy}><h2 id={titleId}>{t('messageCenter.title')}</h2><p>{t('messageCenter.subtitle')}</p></div><Button size="icon" className={styles.close} onClick={closePanel} aria-label={t('messageCenter.close')}><Icon name="close" size={18} strokeWidth={2}/></Button></header>
252-
<div className={styles.controls}><div className={styles.filters} role="group" aria-label={t('messageCenter.title')}>{FILTERS.map((item) => <button key={item.id} type="button" className={`${styles.filter}${filter === item.id ? ` ${styles.filterActive}` : ''}`} aria-pressed={filter === item.id} onClick={() => setFilter(item.id)}>{t(item.label)}{item.id === 'unread' && unreadCount > 0 ? <span className={styles.filterBadge} aria-hidden>{unreadBadgeLabel(unreadCount)}</span> : null}</button>)}</div><button type="button" className={styles.markAll} onClick={() => void markAllRead().catch(() => setSyncState('error'))} disabled={unreadCount === 0}>{t('messageCenter.markAllRead')}</button></div>
239+
<div className={styles.controls}><button type="button" className={styles.markAll} onClick={() => void markAllRead().catch(() => setSyncState('error'))} disabled={unreadCount === 0}>{t('messageCenter.markAllRead')}</button></div>
253240
<div className={styles.list} aria-live="polite">
254241
{syncState === 'error' && messages.length > 0 ? (
255242
<div className={styles.syncStatus} role="status">
@@ -274,7 +261,7 @@ export function MessageCenter({
274261
</button>
275262
</div>
276263
</div>
277-
) : visibleMessages.length === 0 ? <div className={styles.empty}><Icon name="bell" size={20}/><strong>{emptyTitle}</strong><p>{t('messageCenter.emptyBody')}</p></div> : visibleMessages.map((message) => <MessageItem key={message.id} locale={locale} message={message} onRead={markRead} onError={() => setSyncState('error')}/>)}
264+
) : messages.length === 0 ? <div className={styles.empty}><Icon name="bell" size={20}/><strong>{t('messageCenter.emptyAllTitle')}</strong><p>{t('messageCenter.emptyBody')}</p></div> : messages.map((message) => <MessageItem key={message.id} locale={locale} message={message} onRead={markRead} onError={() => setSyncState('error')}/>)}
278265
</div>
279266
<footer className={styles.footer}><p>{t('messageCenter.desktopSettingsHint')}</p>{onOpenNotificationSettings ? <Button variant="ghost" onClick={() => { closePanel(); onOpenNotificationSettings(); }}>{t('messageCenter.desktopSettings')}</Button> : null}</footer>
280267
</aside></div>, document.body) : null}

apps/web/tests/components/MessageCenter.test.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,20 @@ describe('MessageCenter', () => {
120120
await waitFor(() => expect(vi.mocked(fetch).mock.calls.some(([url, init]) => String(url).includes('/release/read') && init?.method === 'POST')).toBe(true));
121121
});
122122

123-
it('filters messages and marks all read', async () => {
123+
it('shows read and unread messages in one flat list and marks all read', async () => {
124124
renderMessageCenter();
125-
await openCenter();
126-
fireEvent.click(screen.getByRole('button', { name: 'Unread' }));
127-
expect(screen.getByText('OpenDesign 0.14 is available')).toBeTruthy();
128-
expect(screen.queryByText('Credits added')).toBeNull();
129-
fireEvent.click(screen.getByRole('button', { name: 'Mark all read' }));
130-
await waitFor(() => expect(screen.getByText('All caught up')).toBeTruthy());
125+
const dialog = await openCenter();
126+
127+
expect(within(dialog).queryByRole('button', { name: 'All' })).toBeNull();
128+
expect(within(dialog).queryByRole('button', { name: 'Unread' })).toBeNull();
129+
expect(within(dialog).queryByRole('button', { name: 'Read' })).toBeNull();
130+
expect(within(dialog).getByText('OpenDesign 0.14 is available')).toBeTruthy();
131+
expect(within(dialog).getByText('Credits added')).toBeTruthy();
132+
133+
fireEvent.click(within(dialog).getByRole('button', { name: 'Mark all read' }));
134+
await waitFor(() => expect(screen.queryByLabelText(/unread/)).toBeNull());
135+
expect(within(dialog).getByText('OpenDesign 0.14 is available')).toBeTruthy();
136+
expect(within(dialog).getByText('Credits added')).toBeTruthy();
131137
});
132138

133139
it('expands the whole message row and opens its CTA', async () => {

0 commit comments

Comments
 (0)