Skip to content

Commit 4293636

Browse files
aleksandernsilvaricardogarim
authored andcommitted
refactor: Sidebar badges (#37638)
1 parent b0ee148 commit 4293636

33 files changed

+752
-222
lines changed

apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
2-
import { SidebarV2ItemBadge, SidebarV2ItemIcon } from '@rocket.chat/fuselage';
1+
import { SidebarV2ItemIcon } from '@rocket.chat/fuselage';
32
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
43
import type { ComponentProps, ReactElement } from 'react';
54
import { useTranslation } from 'react-i18next';
65

76
import NavBarSearchItem from './NavBarSearchItem';
87
import { RoomIcon } from '../../components/RoomIcon';
98
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
10-
import { OmnichannelBadges } from '../../sidebarv2/badges/OmnichannelBadges';
9+
import SidebarItemBadges from '../../sidebarv2/badges/SidebarItemBadges';
1110
import { useUnreadDisplay } from '../../sidebarv2/hooks/useUnreadDisplay';
1211

1312
type NavBarSearchItemWithDataProps = {
@@ -22,26 +21,10 @@ const NavBarSearchItemWithData = ({ room, AvatarTemplate, ...props }: NavBarSear
2221
const href = roomCoordinator.getRouteLink(room.t, room) || '';
2322
const title = roomCoordinator.getRoomName(room.t, room) || '';
2423

25-
const { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread: highlighted } = useUnreadDisplay(room);
24+
const { unreadTitle, showUnread, highlightUnread: highlighted } = useUnreadDisplay(room);
2625

2726
const icon = <SidebarV2ItemIcon highlighted={highlighted} icon={<RoomIcon room={room} placement='sidebar' size='x20' />} />;
2827

29-
const badges = (
30-
<>
31-
{showUnread && (
32-
<SidebarV2ItemBadge
33-
variant={unreadVariant}
34-
title={unreadTitle}
35-
role='status'
36-
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title })}
37-
>
38-
<span aria-hidden>{unreadCount.total}</span>
39-
</SidebarV2ItemBadge>
40-
)}
41-
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
42-
</>
43-
);
44-
4528
return (
4629
<NavBarSearchItem
4730
{...props}
@@ -50,7 +33,7 @@ const NavBarSearchItemWithData = ({ room, AvatarTemplate, ...props }: NavBarSear
5033
aria-label={showUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title }) : title}
5134
title={title}
5235
icon={icon}
53-
badges={badges}
36+
badges={<SidebarItemBadges room={room} roomTitle={title} />}
5437
avatar={AvatarTemplate}
5538
/>
5639
);

apps/meteor/client/sidebar/RoomList/RoomList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { IRoom } from '@rocket.chat/core-typings';
21
import { css } from '@rocket.chat/css-in-js';
32
import { Box } from '@rocket.chat/fuselage';
43
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
54
import { VirtualizedScrollbars } from '@rocket.chat/ui-client';
5+
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
66
import { useUserPreference, useUserId } from '@rocket.chat/ui-contexts';
77
import type { ReactElement } from 'react';
88
import { useMemo } from 'react';
@@ -19,7 +19,7 @@ import { useRoomList } from '../hooks/useRoomList';
1919
import { useShortcutOpenMenu } from '../hooks/useShortcutOpenMenu';
2020
import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';
2121

22-
const computeItemKey = (index: number, room: IRoom): IRoom['_id'] | number => room._id || index;
22+
const computeItemKey = (index: number, room: SubscriptionWithRoom): SubscriptionWithRoom['_id'] | number => room._id || index;
2323

2424
const RoomList = (): ReactElement => {
2525
const { t } = useTranslation();

apps/meteor/client/sidebar/RoomList/RoomListRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
21
import { SidebarSection } from '@rocket.chat/fuselage';
2+
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
33
import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '@rocket.chat/ui-video-conf';
44
import type { TFunction } from 'i18next';
55
import type { ReactElement } from 'react';
@@ -19,7 +19,7 @@ type RoomListRowProps = {
1919
isAnonymous: boolean;
2020
};
2121

22-
const RoomListRow = ({ data, item }: { data: RoomListRowProps; item: ISubscription & IRoom }): ReactElement => {
22+
const RoomListRow = ({ data, item }: { data: RoomListRowProps; item: SubscriptionWithRoom }): ReactElement => {
2323
const { extended, t, SideBarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data;
2424

2525
const acceptCall = useVideoConfAcceptCall();

apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx

Lines changed: 13 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings';
1+
import type { IMessage } from '@rocket.chat/core-typings';
22
import { isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom, isVideoConfMessage } from '@rocket.chat/core-typings';
3-
import { Badge, Sidebar, SidebarItemAction, SidebarItemActions, Margins } from '@rocket.chat/fuselage';
3+
import { Sidebar, SidebarItemAction, SidebarItemActions } from '@rocket.chat/fuselage';
4+
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
45
import { useLayout } from '@rocket.chat/ui-contexts';
56
import DOMPurify from 'dompurify';
67
import type { TFunction } from 'i18next';
@@ -13,10 +14,11 @@ import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
1314
import { isIOsDevice } from '../../lib/utils/isIOsDevice';
1415
import { useOmnichannelPriorities } from '../../views/omnichannel/hooks/useOmnichannelPriorities';
1516
import RoomMenu from '../RoomMenu';
16-
import { OmnichannelBadges } from '../badges/OmnichannelBadges';
17+
import SidebarItemBadges from '../badges/SidebarItemBadges';
1718
import type { useAvatarTemplate } from '../hooks/useAvatarTemplate';
19+
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';
1820

19-
const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: TFunction): string | undefined => {
21+
const getMessage = (room: SubscriptionWithRoom, lastMessage: IMessage | undefined, t: TFunction): string | undefined => {
2022
if (!lastMessage) {
2123
return t('No_messages_yet');
2224
}
@@ -35,24 +37,6 @@ const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: TFunction
3537
return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`;
3638
};
3739

38-
const getBadgeTitle = (userMentions: number, threadUnread: number, groupMentions: number, unread: number, t: TFunction) => {
39-
const title = [] as string[];
40-
if (userMentions) {
41-
title.push(t('mentions_counter', { count: userMentions }));
42-
}
43-
if (threadUnread) {
44-
title.push(t('threads_counter', { count: threadUnread }));
45-
}
46-
if (groupMentions) {
47-
title.push(t('group_mentions_counter', { count: groupMentions }));
48-
}
49-
const count = unread - userMentions - groupMentions;
50-
if (count > 0) {
51-
title.push(t('unread_messages_counter', { count }));
52-
}
53-
return title.join(', ');
54-
};
55-
5640
type RoomListRowProps = {
5741
extended: boolean;
5842
t: TFunction;
@@ -80,7 +64,7 @@ type RoomListRowProps = {
8064
// sidebarViewMode: 'extended';
8165
isAnonymous?: boolean;
8266

83-
room: ISubscription & IRoom;
67+
room: SubscriptionWithRoom;
8468
id?: string;
8569
/* @deprecated */
8670
style?: AllHTMLAttributes<HTMLElement>['style'];
@@ -110,22 +94,10 @@ function SideBarItemTemplateWithData({
11094
const href = roomCoordinator.getRouteLink(room.t, room) || '';
11195
const title = roomCoordinator.getRoomName(room.t, room) || '';
11296

113-
const {
114-
lastMessage,
115-
hideUnreadStatus,
116-
hideMentionStatus,
117-
unread = 0,
118-
alert,
119-
userMentions,
120-
groupMentions,
121-
tunread = [],
122-
tunreadUser = [],
123-
rid,
124-
t: type,
125-
cl,
126-
} = room;
97+
const { lastMessage, unread = 0, alert, rid, t: type, cl } = room;
98+
99+
const { unreadCount, unreadTitle, showUnread, highlightUnread: highlighted } = useUnreadDisplay(room);
127100

128-
const highlighted = Boolean(!hideUnreadStatus && (alert || unread));
129101
const icon = (
130102
// TODO: Remove icon='at'
131103
<Sidebar.Item.Icon highlighted={highlighted} icon='at'>
@@ -152,32 +124,6 @@ function SideBarItemTemplateWithData({
152124
<span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(message) }} />
153125
) : null;
154126

155-
const threadUnread = tunread.length > 0;
156-
const variant =
157-
((userMentions || tunreadUser.length) && 'danger') || (threadUnread && 'primary') || (groupMentions && 'warning') || 'secondary';
158-
159-
const isUnread = unread > 0 || threadUnread;
160-
const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0));
161-
162-
const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t);
163-
164-
const badges = (
165-
<Margins inlineStart={8}>
166-
{showBadge && isUnread && (
167-
<Badge
168-
role='status'
169-
{...({ style: { display: 'inline-flex', flexShrink: 0 } } as any)}
170-
variant={variant}
171-
title={badgeTitle}
172-
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle: badgeTitle, roomTitle: title })}
173-
>
174-
<span aria-hidden>{unread + tunread?.length}</span>
175-
</Badge>
176-
)}
177-
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
178-
</Margins>
179-
);
180-
181127
return (
182128
<SideBarItemTemplate
183129
is='a'
@@ -190,21 +136,21 @@ function SideBarItemTemplateWithData({
190136
onClick={(): void => {
191137
!selected && sidebar.toggle();
192138
}}
193-
aria-label={showBadge && isUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle: badgeTitle, roomTitle: title }) : title}
139+
aria-label={showUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title }) : title}
194140
title={title}
195141
time={lastMessage?.ts}
196142
subtitle={subtitle}
197143
icon={icon}
198144
style={style}
199-
badges={badges}
145+
badges={<SidebarItemBadges room={room} roomTitle={title} />}
200146
avatar={AvatarTemplate && <AvatarTemplate {...room} />}
201147
actions={actions}
202148
menu={
203149
!isIOsDevice && !isAnonymous && (!isQueued || (isQueued && isPriorityEnabled))
204150
? (): ReactElement => (
205151
<RoomMenu
206152
alert={alert}
207-
threadUnread={threadUnread}
153+
threadUnread={unreadCount.threads > 0}
208154
rid={rid}
209155
unread={!!unread}
210156
roomOpen={selected}

apps/meteor/client/sidebar/badges/OmnichannelBadges.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import SidebarItemBadges from './SidebarItemBadges';
5+
import { createFakeSubscription } from '../../../tests/mocks/data';
6+
7+
jest.mock('../../views/omnichannel/components/OmnichannelBadges', () => ({
8+
__esModule: true,
9+
default: () => <i role='status' aria-label='OmnichannelBadges' />,
10+
}));
11+
12+
describe('SidebarItemBadges', () => {
13+
const appRoot = mockAppRoot()
14+
.withTranslations('en', 'core', {
15+
Message_request: 'Message request',
16+
mentions_counter_one: '{{count}} mention',
17+
mentions_counter_other: '{{count}} mentions',
18+
__unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}',
19+
})
20+
.build();
21+
22+
afterEach(() => {
23+
jest.resetAllMocks();
24+
});
25+
26+
it('should render UnreadBadge when there are unread messages', () => {
27+
render(<SidebarItemBadges room={createFakeSubscription({ unread: 1, userMentions: 1, groupMentions: 0 })} roomTitle='Test Room' />, {
28+
wrapper: appRoot,
29+
});
30+
31+
expect(screen.getByRole('status', { name: '1 mention from Test Room' })).toBeInTheDocument();
32+
});
33+
34+
it('should not render UnreadBadge when there are no unread messages', () => {
35+
render(<SidebarItemBadges room={createFakeSubscription({ unread: 0, userMentions: 0, groupMentions: 0 })} roomTitle='Test Room' />, {
36+
wrapper: appRoot,
37+
});
38+
39+
expect(screen.queryByRole('status', { name: 'Test Room' })).not.toBeInTheDocument();
40+
});
41+
42+
it('should render OmnichannelBadges when the room is an omnichannel room', () => {
43+
render(<SidebarItemBadges room={createFakeSubscription({ t: 'l' })} />, { wrapper: appRoot });
44+
45+
expect(screen.getByRole('status', { name: 'OmnichannelBadges' })).toBeInTheDocument();
46+
});
47+
48+
it('should not render OmnichannelBadges when the room is not an omnichannel room', () => {
49+
render(<SidebarItemBadges room={createFakeSubscription({ t: 'p' })} />, { wrapper: appRoot });
50+
51+
expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
52+
});
53+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
2+
import { Margins } from '@rocket.chat/fuselage';
3+
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
4+
5+
import UnreadBadge from './UnreadBadge';
6+
import OmnichannelBadges from '../../views/omnichannel/components/OmnichannelBadges';
7+
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';
8+
9+
type SidebarItemBadgesProps = {
10+
room: SubscriptionWithRoom;
11+
roomTitle?: string;
12+
};
13+
14+
const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
15+
const { unreadCount, unreadTitle, unreadVariant, showUnread } = useUnreadDisplay(room);
16+
17+
return (
18+
<Margins inlineStart={8}>
19+
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
20+
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
21+
</Margins>
22+
);
23+
};
24+
25+
export default SidebarItemBadges;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Badge } from '@rocket.chat/fuselage';
2+
import { useTranslation } from 'react-i18next';
3+
4+
type UnreadBadgeProps = {
5+
title: string;
6+
roomTitle?: string;
7+
variant: 'primary' | 'warning' | 'danger' | 'secondary';
8+
total: number;
9+
};
10+
11+
const UnreadBadge = ({ title, variant, total, roomTitle }: UnreadBadgeProps) => {
12+
const { t } = useTranslation();
13+
14+
return (
15+
<Badge
16+
role='status'
17+
{...({ style: { display: 'inline-flex', flexShrink: 0 } } as any)}
18+
variant={variant}
19+
title={title}
20+
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle: title, roomTitle })}
21+
>
22+
<span aria-hidden>{total}</span>
23+
</Badge>
24+
);
25+
};
26+
27+
export default UnreadBadge;

apps/meteor/client/sidebar/hooks/useAvatarTemplate.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { IRoom } from '@rocket.chat/core-typings';
22
import { RoomAvatar } from '@rocket.chat/ui-avatar';
3+
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
34
import { useUserPreference } from '@rocket.chat/ui-contexts';
45
import type { ComponentType } from 'react';
56
import { useMemo } from 'react';
67

8+
const isSubscriptionWithRoom = (room: SubscriptionWithRoom | IRoom): room is SubscriptionWithRoom => 'rid' in room;
9+
710
export const useAvatarTemplate = (
811
sidebarViewMode?: 'extended' | 'medium' | 'condensed',
912
sidebarDisplayAvatar?: boolean,
10-
): null | ComponentType<IRoom & { rid: string }> => {
13+
): ComponentType<SubscriptionWithRoom | IRoom> | null => {
1114
const sidebarViewModeFromSettings = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode');
1215
const sidebarDisplayAvatarFromSettings = useUserPreference('sidebarDisplayAvatar');
1316

@@ -30,9 +33,10 @@ export const useAvatarTemplate = (
3033
}
3134
})();
3235

33-
const renderRoomAvatar: ComponentType<IRoom & { rid: string }> = (room) => (
34-
<RoomAvatar size={size} room={{ ...room, _id: room.rid || room._id, type: room.t }} />
35-
);
36+
const renderRoomAvatar: ComponentType<SubscriptionWithRoom | IRoom> = (room) => {
37+
const roomId = isSubscriptionWithRoom(room) ? room.rid : room._id;
38+
return <RoomAvatar size={size} room={{ ...room, _id: roomId, type: room.t }} />;
39+
};
3640

3741
return renderRoomAvatar;
3842
}, [displayAvatar, viewMode]);

0 commit comments

Comments
 (0)