Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9fda01d
refactor(sidebar-v1): SideBarItemTemplateWithData to use hook useUnre…
aleksandernsilva Nov 27, 2025
74e0607
refactor(sidebar-v1): OmnichannelBadges to export default
aleksandernsilva Nov 28, 2025
35185a9
refactor(sidebar-v2): OmnichannelBadges to export default
aleksandernsilva Nov 28, 2025
478b279
refactor(sidebar-v1): SidebarItemBadges
aleksandernsilva Nov 28, 2025
e8d5b25
refactor(sidebar-v2): SidebarItemBadges
aleksandernsilva Nov 28, 2025
4daa2c3
refactor(navigation): SidebarItemBadges
aleksandernsilva Nov 28, 2025
2048cd0
refactor(sidepanel): RoomSidePanelItemBadges
aleksandernsilva Nov 28, 2025
a58b4c3
refactor: exported UnreadData type
aleksandernsilva Nov 28, 2025
d3da04d
chore(navigation): removed unused OmnichannelBadges
aleksandernsilva Nov 28, 2025
59e40c2
refactor(navigation): adjusted import
aleksandernsilva Nov 28, 2025
c75546a
refactor(navbar-v2): NavBarSearchItemWithData
aleksandernsilva Nov 28, 2025
f033c06
test: fixed typo
aleksandernsilva Nov 28, 2025
8e405f2
test: added wrapper
aleksandernsilva Nov 28, 2025
a1e635d
refactor: use highlightUnread
aleksandernsilva Nov 28, 2025
7ca04cd
chore: code style
aleksandernsilva Dec 8, 2025
55b16b9
test: adjusted to use accessible locators
aleksandernsilva Dec 8, 2025
e6658db
refactor: moved OmnichannelBadges to views/omnichannel
aleksandernsilva Dec 9, 2025
5aa2110
refactor(sidebar-v1): replaced ISubscription & IRoom types with Subsc…
aleksandernsilva Dec 10, 2025
bb29143
Merge branch 'develop' into refactor/sidebar-badges
kodiakhq[bot] Dec 11, 2025
5d30f18
Merge branch 'develop' into refactor/sidebar-badges
kodiakhq[bot] Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { SidebarV2ItemBadge, SidebarV2ItemIcon } from '@rocket.chat/fuselage';
import { SidebarV2ItemIcon } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import type { ComponentProps, ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import NavBarSearchItem from './NavBarSearchItem';
import { RoomIcon } from '../../components/RoomIcon';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { OmnichannelBadges } from '../../sidebarv2/badges/OmnichannelBadges';
import SidebarItemBadges from '../../sidebarv2/badges/SidebarItemBadges';
import { useUnreadDisplay } from '../../sidebarv2/hooks/useUnreadDisplay';

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

const { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread: highlighted } = useUnreadDisplay(room);
const { unreadTitle, showUnread, highlightUnread: highlighted } = useUnreadDisplay(room);

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

const badges = (
<>
{showUnread && (
<SidebarV2ItemBadge
variant={unreadVariant}
title={unreadTitle}
role='status'
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title })}
>
<span aria-hidden>{unreadCount.total}</span>
</SidebarV2ItemBadge>
)}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
</>
);

return (
<NavBarSearchItem
{...props}
Expand All @@ -50,7 +33,7 @@ const NavBarSearchItemWithData = ({ room, AvatarTemplate, ...props }: NavBarSear
aria-label={showUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title }) : title}
title={title}
icon={icon}
badges={badges}
badges={<SidebarItemBadges room={room} roomTitle={title} />}
avatar={AvatarTemplate}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/sidebar/RoomList/RoomList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
import { VirtualizedScrollbars } from '@rocket.chat/ui-client';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useUserPreference, useUserId } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useMemo } from 'react';
Expand All @@ -19,7 +19,7 @@ import { useRoomList } from '../hooks/useRoomList';
import { useShortcutOpenMenu } from '../hooks/useShortcutOpenMenu';
import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';

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

const RoomList = (): ReactElement => {
const { t } = useTranslation();
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/sidebar/RoomList/RoomListRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
import { SidebarSection } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '@rocket.chat/ui-video-conf';
import type { TFunction } from 'i18next';
import type { ReactElement } from 'react';
Expand All @@ -19,7 +19,7 @@ type RoomListRowProps = {
isAnonymous: boolean;
};

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

const acceptCall = useVideoConfAcceptCall();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings';
import type { IMessage } from '@rocket.chat/core-typings';
import { isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom, isVideoConfMessage } from '@rocket.chat/core-typings';
import { Badge, Sidebar, SidebarItemAction, SidebarItemActions, Margins } from '@rocket.chat/fuselage';
import { Sidebar, SidebarItemAction, SidebarItemActions } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useLayout } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import type { TFunction } from 'i18next';
Expand All @@ -13,10 +14,11 @@ import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { isIOsDevice } from '../../lib/utils/isIOsDevice';
import { useOmnichannelPriorities } from '../../views/omnichannel/hooks/useOmnichannelPriorities';
import RoomMenu from '../RoomMenu';
import { OmnichannelBadges } from '../badges/OmnichannelBadges';
import SidebarItemBadges from '../badges/SidebarItemBadges';
import type { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';

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

const getBadgeTitle = (userMentions: number, threadUnread: number, groupMentions: number, unread: number, t: TFunction) => {
const title = [] as string[];
if (userMentions) {
title.push(t('mentions_counter', { count: userMentions }));
}
if (threadUnread) {
title.push(t('threads_counter', { count: threadUnread }));
}
if (groupMentions) {
title.push(t('group_mentions_counter', { count: groupMentions }));
}
const count = unread - userMentions - groupMentions;
if (count > 0) {
title.push(t('unread_messages_counter', { count }));
}
return title.join(', ');
};

type RoomListRowProps = {
extended: boolean;
t: TFunction;
Expand Down Expand Up @@ -80,7 +64,7 @@ type RoomListRowProps = {
// sidebarViewMode: 'extended';
isAnonymous?: boolean;

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

const {
lastMessage,
hideUnreadStatus,
hideMentionStatus,
unread = 0,
alert,
userMentions,
groupMentions,
tunread = [],
tunreadUser = [],
rid,
t: type,
cl,
} = room;
const { lastMessage, unread = 0, alert, rid, t: type, cl } = room;

const { unreadCount, unreadTitle, showUnread, highlightUnread: highlighted } = useUnreadDisplay(room);

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

const threadUnread = tunread.length > 0;
const variant =
((userMentions || tunreadUser.length) && 'danger') || (threadUnread && 'primary') || (groupMentions && 'warning') || 'secondary';

const isUnread = unread > 0 || threadUnread;
const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0));

const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t);

const badges = (
<Margins inlineStart={8}>
{showBadge && isUnread && (
<Badge
role='status'
{...({ style: { display: 'inline-flex', flexShrink: 0 } } as any)}
variant={variant}
title={badgeTitle}
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle: badgeTitle, roomTitle: title })}
>
<span aria-hidden>{unread + tunread?.length}</span>
</Badge>
)}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
</Margins>
);

return (
<SideBarItemTemplate
is='a'
Expand All @@ -190,21 +136,21 @@ function SideBarItemTemplateWithData({
onClick={(): void => {
!selected && sidebar.toggle();
}}
aria-label={showBadge && isUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle: badgeTitle, roomTitle: title }) : title}
aria-label={showUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title }) : title}
title={title}
time={lastMessage?.ts}
subtitle={subtitle}
icon={icon}
style={style}
badges={badges}
badges={<SidebarItemBadges room={room} roomTitle={title} />}
avatar={AvatarTemplate && <AvatarTemplate {...room} />}
actions={actions}
menu={
!isIOsDevice && !isAnonymous && (!isQueued || (isQueued && isPriorityEnabled))
? (): ReactElement => (
<RoomMenu
alert={alert}
threadUnread={threadUnread}
threadUnread={unreadCount.threads > 0}
rid={rid}
unread={!!unread}
roomOpen={selected}
Expand Down
21 changes: 0 additions & 21 deletions apps/meteor/client/sidebar/badges/OmnichannelBadges.tsx

This file was deleted.

53 changes: 53 additions & 0 deletions apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';

import SidebarItemBadges from './SidebarItemBadges';
import { createFakeSubscription } from '../../../tests/mocks/data';

jest.mock('../../views/omnichannel/components/OmnichannelBadges', () => ({
__esModule: true,
default: () => <i role='status' aria-label='OmnichannelBadges' />,
}));

describe('SidebarItemBadges', () => {
const appRoot = mockAppRoot()
.withTranslations('en', 'core', {
Message_request: 'Message request',
mentions_counter_one: '{{count}} mention',
mentions_counter_other: '{{count}} mentions',
__unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}',
})
.build();

afterEach(() => {
jest.resetAllMocks();
});

it('should render UnreadBadge when there are unread messages', () => {
render(<SidebarItemBadges room={createFakeSubscription({ unread: 1, userMentions: 1, groupMentions: 0 })} roomTitle='Test Room' />, {
wrapper: appRoot,
});

expect(screen.getByRole('status', { name: '1 mention from Test Room' })).toBeInTheDocument();
});

it('should not render UnreadBadge when there are no unread messages', () => {
render(<SidebarItemBadges room={createFakeSubscription({ unread: 0, userMentions: 0, groupMentions: 0 })} roomTitle='Test Room' />, {
wrapper: appRoot,
});

expect(screen.queryByRole('status', { name: 'Test Room' })).not.toBeInTheDocument();
});

it('should render OmnichannelBadges when the room is an omnichannel room', () => {
render(<SidebarItemBadges room={createFakeSubscription({ t: 'l' })} />, { wrapper: appRoot });

expect(screen.getByRole('status', { name: 'OmnichannelBadges' })).toBeInTheDocument();
});

it('should not render OmnichannelBadges when the room is not an omnichannel room', () => {
render(<SidebarItemBadges room={createFakeSubscription({ t: 'p' })} />, { wrapper: appRoot });

expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});
});
25 changes: 25 additions & 0 deletions apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Margins } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';

import UnreadBadge from './UnreadBadge';
import OmnichannelBadges from '../../views/omnichannel/components/OmnichannelBadges';
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';

type SidebarItemBadgesProps = {
room: SubscriptionWithRoom;
roomTitle?: string;
};

const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
const { unreadCount, unreadTitle, unreadVariant, showUnread } = useUnreadDisplay(room);

return (
<Margins inlineStart={8}>
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
</Margins>
);
};

export default SidebarItemBadges;
27 changes: 27 additions & 0 deletions apps/meteor/client/sidebar/badges/UnreadBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Badge } from '@rocket.chat/fuselage';
import { useTranslation } from 'react-i18next';

type UnreadBadgeProps = {
title: string;
roomTitle?: string;
variant: 'primary' | 'warning' | 'danger' | 'secondary';
total: number;
};

const UnreadBadge = ({ title, variant, total, roomTitle }: UnreadBadgeProps) => {
const { t } = useTranslation();

return (
<Badge
role='status'
{...({ style: { display: 'inline-flex', flexShrink: 0 } } as any)}
variant={variant}
title={title}
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle: title, roomTitle })}
>
<span aria-hidden>{total}</span>
</Badge>
);
};

export default UnreadBadge;
12 changes: 8 additions & 4 deletions apps/meteor/client/sidebar/hooks/useAvatarTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useUserPreference } from '@rocket.chat/ui-contexts';
import type { ComponentType } from 'react';
import { useMemo } from 'react';

const isSubscriptionWithRoom = (room: SubscriptionWithRoom | IRoom): room is SubscriptionWithRoom => 'rid' in room;

export const useAvatarTemplate = (
sidebarViewMode?: 'extended' | 'medium' | 'condensed',
sidebarDisplayAvatar?: boolean,
): null | ComponentType<IRoom & { rid: string }> => {
): ComponentType<SubscriptionWithRoom | IRoom> | null => {
const sidebarViewModeFromSettings = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode');
const sidebarDisplayAvatarFromSettings = useUserPreference('sidebarDisplayAvatar');

Expand All @@ -30,9 +33,10 @@ export const useAvatarTemplate = (
}
})();

const renderRoomAvatar: ComponentType<IRoom & { rid: string }> = (room) => (
<RoomAvatar size={size} room={{ ...room, _id: room.rid || room._id, type: room.t }} />
);
const renderRoomAvatar: ComponentType<SubscriptionWithRoom | IRoom> = (room) => {
const roomId = isSubscriptionWithRoom(room) ? room.rid : room._id;
return <RoomAvatar size={size} room={{ ...room, _id: roomId, type: room.t }} />;
};

return renderRoomAvatar;
}, [displayAvatar, viewMode]);
Expand Down
Loading
Loading