snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import { memo } from 'react';
import { createSidePanel } from './SidePanelInternal';
import RoomSidePanelItem from './SidepanelItem/RoomSidePanelItem';
const SidePanel = createSidePanel(RoomSidePanelItem);
export default memo(SidePanel);

View File

@@ -0,0 +1,8 @@
import { memo } from 'react';
import { createSidePanel } from './SidePanelInternal';
import InquireSidePanelItem from './omnichannel/InquireSidePanelItem';
const SidePanelInquiry = createSidePanel(InquireSidePanelItem);
export default memo(SidePanelInquiry);

View File

@@ -0,0 +1,85 @@
import { Box, IconButton, Sidepanel, SidepanelHeader, SidepanelHeaderTitle, SidepanelListItem, ToggleSwitch } from '@rocket.chat/fuselage';
import { VirtualizedScrollbars } from '@rocket.chat/ui-client';
import { useLayout } from '@rocket.chat/ui-contexts';
import { useId, useRef, type ComponentType } from 'react';
import { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso';
import SidePanelNoResults from './SidePanelNoResults';
import SidepanelListWrapper from './SidepanelListWrapper';
import { useOpenedRoom } from '../../../lib/RoomManager';
import { useIsRoomFilter, type AllGroupsKeys } from '../contexts/RoomsNavigationContext';
import { usePreventDefault } from '../sidebar/hooks/usePreventDefault';
type SidePanelProps<R = any> = {
title: string;
currentTab: AllGroupsKeys;
unreadOnly: boolean;
toggleUnreadOnly: () => void;
rooms: R[];
ItemContentComponent: ComponentType<{
room: R;
openedRoom: ReturnType<typeof useOpenedRoom>;
isRoomFilter: boolean;
}>;
};
const SidePanelInternal = ({ title, currentTab, unreadOnly, toggleUnreadOnly, rooms, ItemContentComponent }: SidePanelProps) => {
const { t } = useTranslation();
const ref = useRef(null);
const unreadFieldId = useId();
const openedRoom = useOpenedRoom();
const {
isTablet,
sidePanel: { closeSidePanel },
} = useLayout();
const isRoomFilter = useIsRoomFilter();
usePreventDefault(ref);
return (
<Sidepanel role='tabpanel' aria-label={t('Side_panel')}>
<SidepanelHeader role='heading' aria-label={title}>
<Box withTruncatedText display='flex' alignItems='center'>
{isTablet && <IconButton marginInlineEnd={8} icon='arrow-back' title={t('Back')} small onClick={closeSidePanel} />}
<SidepanelHeaderTitle>{title}</SidepanelHeaderTitle>
</Box>
<Box display='flex' alignItems='center'>
<Box htmlFor={unreadFieldId} is='label' fontScale='c1' marginInlineEnd={8}>
{t('Unread')}
</Box>
<ToggleSwitch id={unreadFieldId} checked={unreadOnly} onChange={toggleUnreadOnly} />
</Box>
</SidepanelHeader>
<Box height='full' ref={ref}>
{rooms && rooms.length === 0 && <SidePanelNoResults currentTab={currentTab} />}
<VirtualizedScrollbars>
<Virtuoso
totalCount={rooms.length}
data={rooms}
components={{ Item: SidepanelListItem, List: SidepanelListWrapper }}
itemContent={(_, room) => {
return <ItemContentComponent room={room} openedRoom={openedRoom} isRoomFilter={isRoomFilter} />;
}}
/>
</VirtualizedScrollbars>
</Box>
</Sidepanel>
);
};
export const createSidePanel =
<R extends object>(
ItemContentComponent: ComponentType<{ room: R; openedRoom: ReturnType<typeof useOpenedRoom>; isRoomFilter: boolean }>,
) =>
// eslint-disable-next-line react/no-multi-comp, react/display-name
({ title, currentTab, unreadOnly, toggleUnreadOnly, rooms }: Omit<SidePanelProps<R>, 'ItemContentComponent'>) => (
<SidePanelInternal
title={title}
currentTab={currentTab}
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
ItemContentComponent={ItemContentComponent}
/>
);

View File

@@ -0,0 +1,70 @@
import { useTranslation } from 'react-i18next';
import GenericNoResults from '../../../components/GenericNoResults';
import { sidePanelFiltersConfig } from '../contexts/RoomsNavigationContext';
import type { AllGroupsKeys } from '../contexts/RoomsNavigationContext';
export type SidePanelNoResultsProps = { currentTab: AllGroupsKeys };
const SidePanelNoResults = ({ currentTab }: SidePanelNoResultsProps) => {
const { t } = useTranslation();
switch (currentTab) {
case 'mentions':
return (
<GenericNoResults icon={sidePanelFiltersConfig.mentions.icon} title={t('No_mentions')} description={t('No_mentions_description')} />
);
case 'favorites':
return (
<GenericNoResults
icon={sidePanelFiltersConfig.favorites.icon}
title={t('No_favorite_rooms')}
description={t('No_favorite_rooms_description')}
/>
);
case 'discussions':
return (
<GenericNoResults
icon={sidePanelFiltersConfig.discussions.icon}
title={t('No_discussions')}
description={t('No_discussions_description')}
/>
);
case 'inProgress':
return (
<GenericNoResults
icon={sidePanelFiltersConfig.inProgress.icon}
title={t('No_chats_in_progress')}
description={t('No_chats_in_progress_description')}
/>
);
case 'queue':
return (
<GenericNoResults
icon={sidePanelFiltersConfig.queue.icon}
title={t('No_chats_in_queue')}
description={t('No_chats_in_queue_description')}
/>
);
case 'onHold':
return (
<GenericNoResults
icon={sidePanelFiltersConfig.onHold.icon}
title={t('No_chats_on_hold')}
description={t('No_chats_on_hold_description')}
/>
);
case 'teams':
return (
<GenericNoResults icon={null} title={t('No_channels_or_discussions')} description={t('No_channels_or_discussions_description')} />
);
case 'channels':
return <GenericNoResults icon={null} title={t('No_discussions')} description={t('No_discussions_channels_filter_description')} />;
case 'directMessages':
return <GenericNoResults icon={null} title={t('No_discussions')} description={t('No_discussions_dms_filter_description')} />;
default:
return <GenericNoResults icon='inbox' title={t('No_rooms')} description={t('No_rooms_description')} />;
}
};
export default SidePanelNoResults;

View File

@@ -0,0 +1,36 @@
import SidePanelAll from './tabs/SidePanelAll';
import { useRoomsListContext, useSidePanelFilter } from '../contexts/RoomsNavigationContext';
import SidePanelInProgress from './omnichannel/tabs/SidePanelInProgress';
import SidePanelQueue from './omnichannel/tabs/SidePanelQueue';
import SidePanelOnHold from './omnichannel/tabs/SidepanelOnHold';
import SidePanelDiscussions from './tabs/SidePanelDiscussions';
import SidePanelFavorites from './tabs/SidePanelFavorites';
import SidePanelRooms from './tabs/SidePanelRooms';
const SidePanelRouter = () => {
const [currentTab] = useSidePanelFilter();
const { parentRid } = useRoomsListContext();
switch (currentTab) {
case 'all':
return <SidePanelAll />;
case 'favorites':
return <SidePanelFavorites />;
case 'discussions':
return <SidePanelDiscussions />;
case 'teams':
case 'channels':
case 'directMessages':
return parentRid ? <SidePanelRooms parentRid={parentRid} /> : <SidePanelAll />;
case 'inProgress':
return <SidePanelInProgress />;
case 'onHold':
return <SidePanelOnHold />;
case 'queue':
return <SidePanelQueue />;
default:
return <SidePanelAll />;
}
};
export default SidePanelRouter;

View File

@@ -0,0 +1,31 @@
import type { RoomType } from '@rocket.chat/core-typings';
import { GenericMenu } from '@rocket.chat/ui-client';
import type { LocationPathname } from '@rocket.chat/ui-contexts';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { memo } from 'react';
import { useRoomMenuActions } from '../hooks/useRoomMenuActions';
export type RoomMenuProps = {
rid: string;
unread?: boolean;
threadUnread?: boolean;
alert?: boolean;
roomOpen?: boolean;
type: RoomType;
cl?: boolean;
name?: string;
hideDefaultOptions: boolean;
href: LocationPathname | undefined;
};
const RoomMenu = ({ rid, unread, threadUnread, alert, roomOpen, type, cl, name = '', hideDefaultOptions = false, href }: RoomMenuProps) => {
const t = useTranslation();
const isUnread = alert || unread || threadUnread;
const sections = useRoomMenuActions({ rid, type, name, isUnread, cl, roomOpen, hideDefaultOptions, href });
return <GenericMenu detached title={t('Options')} mini aria-keyshortcuts='alt' sections={sections} />;
};
export default memo(RoomMenu);

View File

@@ -0,0 +1,82 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Icon, SidebarItemIcon } from '@rocket.chat/fuselage';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import { useUserId, type SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import RoomMenu from './RoomMenu';
import RoomSidePanelItemBadges from './RoomSidePanelItemBadges';
import SidePanelParent from './SidePanelParent';
import SidePanelItem from './SidepanelItem';
import { RoomIcon } from '../../../../components/RoomIcon';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
import { getSubscriptionDraft } from '../../../../lib/utils/getSubscriptionDraft';
import { isIOsDevice } from '../../../../lib/utils/isIOsDevice';
import { getMessagePreview } from '../../../../lib/utils/normalizeMessagePreview/getMessagePreview';
import { useOmnichannelPriorities } from '../../../omnichannel/hooks/useOmnichannelPriorities';
import { useUnreadDisplay } from '../../sidebar/hooks/useUnreadDisplay';
export type RoomSidePanelItemProps = {
room: SubscriptionWithRoom;
openedRoom?: string;
isRoomFilter?: boolean;
};
const RoomSidePanelItem = ({ room, openedRoom, isRoomFilter, ...props }: RoomSidePanelItemProps) => {
const { t } = useTranslation();
const isAnonymous = !useUserId();
const { highlightUnread: highlighted, unreadCount } = useUnreadDisplay(room);
const { unread = 0, alert, rid, t: type, cl } = room;
const time = 'lastMessage' in room ? room.lastMessage?.ts : undefined;
const message = getMessagePreview(room, room.lastMessage, t);
const title = roomCoordinator.getRoomName(room.t, room) || '';
const href = roomCoordinator.getRouteLink(room.t, room) || '';
const isQueued = isOmnichannelRoom(room) && room.status === 'queued';
const { enabled: isPriorityEnabled } = useOmnichannelPriorities();
const parentRoomId = Boolean(room.prid || (room.teamId && !room.teamMain));
const titleIcon = getSubscriptionDraft(room) ? (
<Icon name='pencil' size='x12' title={room.draft ? t('Unfinished_message') : t('Unfinished_thread_message')} />
) : undefined;
const menu =
!isIOsDevice && !isAnonymous && (!isQueued || (isQueued && isPriorityEnabled)) ? (
<RoomMenu
alert={alert}
threadUnread={unreadCount.threads > 0}
rid={rid}
unread={!!unread}
roomOpen={rid === openedRoom}
type={type}
cl={cl}
name={title}
hideDefaultOptions={isQueued}
href={href || undefined}
/>
) : undefined;
return (
<SidePanelItem
href={href}
selected={rid === openedRoom}
title={title}
titleIcon={titleIcon}
avatar={<RoomAvatar size='x20' room={{ ...room, _id: room.rid || room._id, type: room.t }} />}
icon={<SidebarItemIcon highlighted={highlighted} icon={<RoomIcon room={room} placement='sidebar' size='x20' />} />}
unread={highlighted}
time={time}
subtitle={message ? <span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: message }} /> : null}
parentRoom={!isRoomFilter && parentRoomId && <SidePanelParent room={room} />}
badges={<RoomSidePanelItemBadges room={room} roomTitle={title} />}
menu={menu}
{...props}
/>
);
};
export default memo(RoomSidePanelItem);

View File

@@ -0,0 +1,78 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';
import RoomSidePanelItemBadges from './RoomSidePanelItemBadges';
import { createFakeSubscription } from '../../../../../tests/mocks/data';
jest.mock('../omnichannel/SidePanelOmnichannelBadges', () => ({
__esModule: true,
default: () => <i role='status' aria-label='OmnichannelBadges' />,
}));
describe('RoomSidePanelItemBadges', () => {
const appRoot = mockAppRoot()
.withTranslations('en', 'core', {
Invited__date__: 'Invited {{date}}',
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(
<RoomSidePanelItemBadges 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(
<RoomSidePanelItemBadges 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(<RoomSidePanelItemBadges 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(<RoomSidePanelItemBadges room={createFakeSubscription({ t: 'p' })} />, { wrapper: appRoot });
expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});
it('should render InvitationBadge when subscription has status INVITED', () => {
render(
<RoomSidePanelItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
wrapper: appRoot,
},
);
expect(screen.getByRole('status', { name: 'Invited January 1st, 2025' })).toBeInTheDocument();
});
it('should not render InvitationBadge when subscription does not have status INVITED', () => {
render(<RoomSidePanelItemBadges room={createFakeSubscription()} />, { wrapper: appRoot });
expect(screen.queryByRole('status', { name: 'Invited' })).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,26 @@
import { isInviteSubscription, isOmnichannelRoom } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import InvitationBadge from '../../../../components/InvitationBadge';
import UnreadBadge from '../../sidebar/badges/UnreadBadge';
import { useUnreadDisplay } from '../../sidebar/hooks/useUnreadDisplay';
import SidePanelOmnichannelBadges from '../omnichannel/SidePanelOmnichannelBadges';
export type RoomSidePanelItemBadgesProps = {
room: SubscriptionWithRoom;
roomTitle?: string;
};
const RoomSidePanelItemBadges = ({ room, roomTitle }: RoomSidePanelItemBadgesProps) => {
const { unreadCount, unreadTitle, unreadVariant, showUnread } = useUnreadDisplay(room);
return (
<>
{isOmnichannelRoom(room) && <SidePanelOmnichannelBadges room={room} />}
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isInviteSubscription(room) && <InvitationBadge invitationDate={room.ts} />}
</>
);
};
export default RoomSidePanelItemBadges;

View File

@@ -0,0 +1,17 @@
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { memo } from 'react';
import SidePanelParentRoom from './SidePanelParentRoom';
import SidePanelParentTeam from './SidePanelParentTeam';
export type SidePanelParentProps = { room: SubscriptionWithRoom };
const SidePanelParent = ({ room }: SidePanelParentProps) => {
if (room.prid) {
return <SidePanelParentRoom prid={room.prid} />;
}
return room.teamId && !room.teamMain && <SidePanelParentTeam room={room} />;
};
export default memo(SidePanelParent);

View File

@@ -0,0 +1,22 @@
import type { ISubscription } from '@rocket.chat/core-typings';
import { isPrivateRoom } from '@rocket.chat/core-typings';
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
import SidePanelTag from '../SidePanelTag';
import SidePanelTagIcon from '../SidePanelTagIcon';
export type SidePanelParentRoomProps = { subscription: ISubscription };
const SidePanelParentRoom = ({ subscription }: SidePanelParentRoomProps) => {
const icon = isPrivateRoom(subscription) ? 'hashtag-lock' : 'hashtag';
const roomName = roomCoordinator.getRoomName(subscription?.t, subscription);
return (
<SidePanelTag>
{icon && <SidePanelTagIcon icon={{ name: icon }} />}
{roomName}
</SidePanelTag>
);
};
export default SidePanelParentRoom;

View File

@@ -0,0 +1,17 @@
import { useUserSubscription } from '@rocket.chat/ui-contexts';
import SidePanelParentRoom from './SidePanelParentRoom';
export type SidePanelParentRoomWithDataProps = { prid: string };
const SidePanelParentRoomWithData = ({ prid }: SidePanelParentRoomWithDataProps) => {
const subscription = useUserSubscription(prid);
if (!subscription) {
return null;
}
return <SidePanelParentRoom subscription={subscription} />;
};
export default SidePanelParentRoomWithData;

View File

@@ -0,0 +1 @@
export { default } from './SidePanelParentRoomWithData';

View File

@@ -0,0 +1,24 @@
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import SidePanelTag from './SidePanelTag';
import SidePanelTagIcon from './SidePanelTagIcon';
import { useParentTeamData } from './useParentTeamData';
export type SidePanelParentTeamProps = { room: SubscriptionWithRoom };
const SidePanelParentTeam = ({ room }: SidePanelParentTeamProps) => {
const { teamName, shouldDisplayTeam, teamInfoError, isTeamPublic } = useParentTeamData(room.teamId);
if (teamInfoError || !shouldDisplayTeam) {
return null;
}
return (
<SidePanelTag>
<SidePanelTagIcon icon={{ name: isTeamPublic ? 'team' : 'team-lock' }} />
{teamName}
</SidePanelTag>
);
};
export default SidePanelParentTeam;

View File

@@ -0,0 +1,8 @@
import { Tag } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const SidePanelTag = (props: ComponentProps<typeof Tag>) => (
<Tag role='button' tabIndex={0} {...props} maxWidth='50%' flexShrink={1} flexGrow={0} />
);
export default SidePanelTag;

View File

@@ -0,0 +1,8 @@
import { Icon } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
export type SidePanelTagIconProps = { icon: Pick<ComponentProps<typeof Icon>, 'name' | 'color'> | null };
const SidePanelTagIcon = ({ icon }: SidePanelTagIconProps) => (icon ? <Icon size='x12' marginInlineEnd={4} {...icon} /> : null);
export default SidePanelTagIcon;

View File

@@ -0,0 +1,90 @@
import {
IconButton,
SidebarItem,
SidebarItemAvatarWrapper,
SidebarItemCol,
SidebarItemContent,
SidebarItemMenu,
SidebarItemRow,
SidebarItemTimestamp,
SidebarItemTitle,
} from '@rocket.chat/fuselage';
import { useLayout } from '@rocket.chat/ui-contexts';
import type { ReactNode } from 'react';
import { memo, useState } from 'react';
import { useShortTimeAgo } from '../../../../hooks/useTimeAgo';
export type SidePanelItemProps = {
href: string;
selected: boolean;
title: string;
titleIcon?: ReactNode;
avatar: ReactNode;
icon: ReactNode;
unread: boolean;
time?: Date;
subtitle: ReactNode;
parentRoom?: ReactNode;
badges?: ReactNode;
menu?: ReactNode;
};
const SidePanelItem = ({
href,
selected,
title,
titleIcon,
avatar,
icon,
unread,
time,
subtitle,
parentRoom,
badges,
menu,
...props
}: SidePanelItemProps) => {
const { sidebar } = useLayout();
const formatDate = useShortTimeAgo();
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
const handleFocus = () => setMenuVisibility(true);
const handlePointerEnter = () => setMenuVisibility(true);
return (
<SidebarItem
{...props}
href={href}
onClick={() => !selected && sidebar.toggle()}
selected={selected}
onFocus={handleFocus}
onPointerEnter={handlePointerEnter}
aria-label={title}
aria-current={selected ? 'page' : undefined}
level={2}
>
<SidebarItemCol>
<SidebarItemRow>
{avatar && <SidebarItemAvatarWrapper>{avatar}</SidebarItemAvatarWrapper>}
{icon}
<SidebarItemTitle unread={unread}>{title}</SidebarItemTitle>
{time && <SidebarItemTimestamp unread={unread}>{formatDate(time)}</SidebarItemTimestamp>}
</SidebarItemRow>
<SidebarItemRow>
<SidebarItemContent unread={unread}>{subtitle}</SidebarItemContent>
{parentRoom}
{titleIcon}
{badges}
{menu && (
<SidebarItemMenu>
{menuVisibility ? menu : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-item__menu icon='kebab' />}
</SidebarItemMenu>
)}
</SidebarItemRow>
</SidebarItemCol>
</SidebarItem>
);
};
export default memo(SidePanelItem);

View File

@@ -0,0 +1 @@
export { default } from './SidepanelItem';

View File

@@ -0,0 +1,40 @@
import type { ITeam } from '@rocket.chat/core-typings';
import { TeamType } from '@rocket.chat/core-typings';
import { useUserId } from '@rocket.chat/ui-contexts';
import { useTeamInfoQuery } from '../../../../hooks/useTeamInfoQuery';
import { useUserTeamsQuery } from '../../../room/hooks/useUserTeamsQuery';
type APIErrorResult = { success: boolean; error: string };
export const useParentTeamData = (teamId?: ITeam['_id']) => {
const userId = useUserId();
if (!teamId) {
throw new Error('invalid rid');
}
if (!userId) {
throw new Error('invalid uid');
}
const {
data: teamInfo,
isLoading: teamInfoLoading,
isError: teamInfoError,
} = useTeamInfoQuery(teamId, { retry: (_, error) => (error as unknown as APIErrorResult)?.error !== 'unauthorized' });
const { data: userTeams, isLoading: userTeamsLoading } = useUserTeamsQuery(userId);
const userBelongsToTeam = Boolean(userTeams?.find((team) => team._id === teamId)) || false;
const isTeamPublic = teamInfo?.type === TeamType.PUBLIC;
const shouldDisplayTeam = isTeamPublic || userBelongsToTeam;
return {
teamName: teamInfo?.name,
isLoading: userTeamsLoading || teamInfoLoading,
teamInfoError,
shouldDisplayTeam,
isTeamPublic,
};
};

View File

@@ -0,0 +1,17 @@
import { SidepanelList } from '@rocket.chat/fuselage';
import { useMergedRefs } from '@rocket.chat/fuselage-hooks';
import { forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { Components } from 'react-virtuoso';
import { useSidebarListNavigation } from '../sidebar/RoomList/useSidebarListNavigation';
const SidepanelListWrapper: Components['List'] = forwardRef(function SidepanelListWrapper(props, ref) {
const { t } = useTranslation();
const { sidebarListRef } = useSidebarListNavigation();
const mergedRefs = useMergedRefs(ref, sidebarListRef);
return <SidepanelList aria-label={t('Channels')} ref={mergedRefs} {...props} />;
});
export default SidepanelListWrapper;

View File

@@ -0,0 +1,126 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { renderHook } from '@testing-library/react';
import { createFakeSubscription } from '../../../../../tests/mocks/data';
import { emptyUnread, createMockGlobalStore } from '../../../../../tests/mocks/utils';
import { isUnreadSubscription } from '../../contexts/RoomsNavigationContext';
const fakeTeam = { ...createFakeSubscription({ alert: true }) };
const fakeChannel = { ...createFakeSubscription({ t: 'c', alert: true }) };
const teamChildChannel = {
...createFakeSubscription({ t: 'c', ...emptyUnread, prid: fakeTeam.rid }),
};
const unreadTeamChildChannel = {
...createFakeSubscription({ t: 'c', alert: true, prid: fakeTeam.rid }),
};
const childChannel = {
...createFakeSubscription({ t: 'c', ...emptyUnread, prid: fakeChannel.rid }),
};
const userMentionsChildChannel = {
...createFakeSubscription({ t: 'c', userMentions: 1, prid: fakeChannel.rid }),
};
const groupMentionsChildChannel = {
...createFakeSubscription({ t: 'c', groupMentions: 1, prid: fakeChannel.rid }),
};
const unreadChildChannel = {
...createFakeSubscription({ t: 'c', unread: 1, prid: fakeChannel.rid }),
};
const tUnreadChildChannel = {
...createFakeSubscription({ t: 'c', tunread: ['1'], prid: fakeChannel.rid }),
};
const tUnreadUserChildChannel = {
...createFakeSubscription({ t: 'c', tunreadUser: ['1'], prid: fakeChannel.rid }),
};
const tUnreadGroupChildChannel = {
...createFakeSubscription({ t: 'c', tunreadGroup: ['1'], prid: fakeChannel.rid }),
};
const teamsSubscriptions = [fakeTeam, teamChildChannel, unreadTeamChildChannel] as unknown as SubscriptionWithRoom[];
const channelsSubscriptions = [
fakeChannel,
childChannel,
userMentionsChildChannel,
groupMentionsChildChannel,
unreadChildChannel,
tUnreadChildChannel,
tUnreadUserChildChannel,
tUnreadGroupChildChannel,
] as unknown as SubscriptionWithRoom[];
const subscriptionsMock = [...teamsSubscriptions, ...channelsSubscriptions] as unknown as SubscriptionWithRoom[];
jest.mock('../../../../stores/Subscriptions', () => {
return {
Subscriptions: createMockGlobalStore(subscriptionsMock),
};
});
it('should return all subscriptions parent of fakeChannel', async () => {
const { useChannelsChildrenList } = await import('./useChannelsChildrenList');
const { result } = renderHook(() => useChannelsChildrenList(fakeChannel.rid, false), {
wrapper: mockAppRoot().withSubscriptions(subscriptionsMock).build(),
});
expect(result.current).toHaveLength(channelsSubscriptions.length);
expect(result.current[0].rid).toBe(fakeChannel.rid);
if (result.current[0].rid === fakeChannel.rid) return;
expect(result.current[0].prid).toBe(fakeChannel.rid);
});
it('should return unread subscriptions after parent but before read ones for channels', async () => {
const { useChannelsChildrenList } = await import('./useChannelsChildrenList');
const { result } = renderHook(() => useChannelsChildrenList(fakeChannel.rid, true), {
wrapper: mockAppRoot().withSubscriptions(subscriptionsMock).build(),
});
expect(result.current).toHaveLength(channelsSubscriptions.length);
expect(result.current[0].rid).toBe(fakeChannel.rid);
expect(isUnreadSubscription(result.current[1])).toBe(true);
expect(isUnreadSubscription(result.current[2])).toBe(true);
expect(isUnreadSubscription(result.current[3])).toBe(true);
expect(isUnreadSubscription(result.current[4])).toBe(true);
expect(isUnreadSubscription(result.current[5])).toBe(true);
expect(isUnreadSubscription(result.current[6])).toBe(true);
expect(isUnreadSubscription(result.current[7])).toBe(false);
if (result.current[0].rid === fakeChannel.rid) return;
expect(result.current[0].prid).toBe(fakeChannel.rid);
});
it('should return subscriptions parent of fakeTeam', async () => {
const { useChannelsChildrenList } = await import('./useChannelsChildrenList');
const { result } = renderHook(() => useChannelsChildrenList(fakeTeam.rid, false), {
wrapper: mockAppRoot().withSubscriptions(subscriptionsMock).build(),
});
expect(result.current).toHaveLength(teamsSubscriptions.length);
expect(result.current[0].rid).toBe(fakeTeam.rid);
if (result.current[0].rid === fakeTeam.rid) return;
expect(result.current[0].prid).toBe(fakeTeam.rid);
});
it('should return unread subscriptions after parent but before read ones for teams', async () => {
const { useChannelsChildrenList } = await import('./useChannelsChildrenList');
const { result } = renderHook(() => useChannelsChildrenList(fakeTeam.rid, true), {
wrapper: mockAppRoot().withSubscriptions(subscriptionsMock).build(),
});
expect(result.current).toHaveLength(teamsSubscriptions.length);
expect(result.current[0].rid).toBe(fakeTeam.rid);
expect(isUnreadSubscription(result.current[1])).toBe(true);
expect(isUnreadSubscription(result.current[2])).toBe(false);
if (result.current[0].rid === fakeTeam.rid) return;
expect(result.current[0].prid).toBe(fakeTeam.rid);
});

View File

@@ -0,0 +1,57 @@
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useShallow } from 'zustand/shallow';
import { pipe } from '../../../../lib/cachedStores/pipe';
import { Subscriptions } from '../../../../stores';
import { isUnreadSubscription } from '../../contexts/RoomsNavigationContext';
const sortByLmPipe = pipe<SubscriptionWithRoom>().sortByField('lm', -1);
/**
* This helper function is used to ensure that the main room (main team room or parent's discussion room)
* is always at the top of the list.
*/
const getMainRoomAndSort = (records: SubscriptionWithRoom[], unreadOnly: boolean, teamId?: string) => {
const mainRoom = records.find((record) => (teamId ? record.teamMain : !record.prid));
const filteredRecords = records.filter((record) => mainRoom?.rid !== record.rid);
const sortedRecords = sortByLmPipe.apply(filteredRecords);
const rest = !unreadOnly
? sortedRecords
: sortedRecords
.reduce(
(result, record) => {
if (isUnreadSubscription(record)) {
result[0].push(record);
return result;
}
result[1].push(record);
return result;
},
[[] as SubscriptionWithRoom[], [] as SubscriptionWithRoom[]],
)
.flat();
if (mainRoom) {
rest.unshift(mainRoom);
}
return rest;
};
export const useChannelsChildrenList = (parentRid: string, unreadOnly: boolean, teamId?: string) => {
return Subscriptions.use(
useShallow((state) => {
const records = state.filter((subscription) => {
if (parentRid === subscription.prid || parentRid === subscription.rid) {
return true;
}
if (teamId && subscription.teamId === teamId) {
return true;
}
return false;
});
return getMainRoomAndSort(records, unreadOnly, teamId);
}),
);
};

View File

@@ -0,0 +1,104 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook } from '@testing-library/react';
import { useRoomMenuActions } from './useRoomMenuActions';
import { createFakeSubscription } from '../../../../../tests/mocks/data';
const mockSubscription = createFakeSubscription({ name: 'room1', fname: 'Room 1', t: 'c', disableNotifications: false, rid: 'room1' });
jest.mock('../../../../../client/lib/rooms/roomCoordinator', () => ({
roomCoordinator: {
getRoomDirectives: () => ({
getUiText: () => 'leaveWarning',
}),
},
}));
jest.mock('../../../../../client/lib/LegacyRoomManager', () => ({
LegacyRoomManager: {
close: jest.fn(),
},
}));
// TODO: Update this mock when we get the mocked OmnichannelContext working
jest.mock('../../../omnichannel/hooks/useOmnichannelPrioritiesMenu', () => ({
useOmnichannelPrioritiesMenu: jest.fn(() => [{ id: 'priority', content: 'Priority', icon: 'priority', onClick: jest.fn() }]),
}));
const mockHookProps = {
rid: 'room1',
type: 'c',
name: 'Room 1',
isUnread: true,
cl: true,
roomOpen: true,
hideDefaultOptions: false,
href: '/channel/room1',
} as const;
describe('useRoomMenuActions', () => {
it('should return all menu options for normal rooms', () => {
const { result } = renderHook(() => useRoomMenuActions(mockHookProps), {
wrapper: mockAppRoot()
.withSubscription({ ...mockSubscription, rid: 'room1' })
.withPermission('leave-c')
.withPermission('leave-p')
.withSetting('Favorite_Rooms', true)
.build(),
});
const actions = result.current;
expect(actions).toHaveLength(2);
expect(actions[0].items).toHaveLength(4);
expect(actions[1].title).toBe('Notifications');
expect(actions[1].items).toHaveLength(2);
});
it('should return priorities section for omnichannel room', () => {
const { result } = renderHook(() => useRoomMenuActions({ ...mockHookProps, type: 'l' }), {
wrapper: mockAppRoot()
.withSubscription({ ...mockSubscription, t: 'l' })
.withPermission('leave-c')
.withPermission('leave-p')
.withSetting('Favorite_Rooms', true)
.build(),
});
expect(result.current.length).toBe(2);
expect(result.current[1].title).toBe('Priorities');
expect(result.current.some((section) => section.title === 'Notifications')).toBe(false);
expect(result.current[0].items).toHaveLength(2);
expect(result.current[0].title).toBe('');
});
it('should not return any menu option if hideDefaultOptions', () => {
const { result } = renderHook(() => useRoomMenuActions({ ...mockHookProps, hideDefaultOptions: true }), {
wrapper: mockAppRoot()
.withSubscription(mockSubscription)
.withPermission('leave-c')
.withPermission('leave-p')
.withSetting('Favorite_Rooms', true)
.build(),
});
expect(result.current).toHaveLength(0);
});
it('should not return favorite room option if setting is disabled', () => {
const { result } = renderHook(() => useRoomMenuActions(mockHookProps), {
wrapper: mockAppRoot()
.withSubscription(mockSubscription)
.withPermission('leave-c')
.withPermission('leave-p')
.withSetting('Favorite_Rooms', false)
.build(),
});
const actions = result.current;
expect(actions).toHaveLength(2);
expect(actions[0].items).toHaveLength(3);
expect(actions[0].items.some((item) => item.id === 'toggleFavorite')).toBe(false);
expect(actions[1].title).toBe('Notifications');
expect(actions[1].items).toHaveLength(2);
});
});

View File

@@ -0,0 +1,145 @@
import type { RoomType } from '@rocket.chat/core-typings';
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
import { usePermission, useRouter, useSetting, useUserSubscription } from '@rocket.chat/ui-contexts';
import type { LocationPathname } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLeaveRoomAction } from '../../../../hooks/menuActions/useLeaveRoom';
import { useToggleFavoriteAction } from '../../../../hooks/menuActions/useToggleFavoriteAction';
import { useToggleNotificationAction } from '../../../../hooks/menuActions/useToggleNotificationsAction';
import { useToggleReadAction } from '../../../../hooks/menuActions/useToggleReadAction';
import { useHideRoomAction } from '../../../../hooks/useHideRoomAction';
import { useOmnichannelPrioritiesMenu } from '../../../omnichannel/hooks/useOmnichannelPrioritiesMenu';
type RoomMenuActionsProps = {
rid: string;
type: RoomType;
name: string;
isUnread?: boolean;
cl?: boolean;
roomOpen?: boolean;
hideDefaultOptions: boolean;
href: LocationPathname | undefined;
};
export const useRoomMenuActions = ({
rid,
type,
name,
isUnread,
cl,
roomOpen,
hideDefaultOptions,
href,
}: RoomMenuActionsProps): { title: string; items: GenericMenuItemProps[] }[] => {
const { t } = useTranslation();
const subscription = useUserSubscription(rid);
const router = useRouter();
const isFavorite = Boolean(subscription?.f);
const canLeaveChannel = usePermission('leave-c');
const canLeavePrivate = usePermission('leave-p');
const canFavorite = useSetting('Favorite_Rooms', true);
const isNotificationEnabled = !subscription?.disableNotifications;
const canLeave = ((): boolean => {
if (type === 'c' && !canLeaveChannel) {
return false;
}
if (type === 'p' && !canLeavePrivate) {
return false;
}
return !((cl != null && !cl) || ['d', 'l'].includes(type));
})();
const handleHide = useHideRoomAction({ rid, type, name }, { redirect: false });
const handleToggleFavorite = useToggleFavoriteAction({ rid, isFavorite });
const handleToggleRead = useToggleReadAction({ rid, isUnread, subscription });
const handleLeave = useLeaveRoomAction({ rid, type, name, roomOpen });
const handleToggleNotification = useToggleNotificationAction({ rid, isNotificationEnabled, roomName: name });
const isOmnichannelRoom = type === 'l';
const prioritiesMenu = useOmnichannelPrioritiesMenu(rid);
const roomMenuOptions = useMemo<GenericMenuItemProps[]>(() => {
if (hideDefaultOptions || !subscription) {
return [];
}
const options: (GenericMenuItemProps | false)[] = [
!isOmnichannelRoom && {
id: 'hideRoom',
icon: 'eye-off',
content: t('Hide'),
onClick: handleHide,
},
{
id: 'toggleRead',
icon: 'flag',
content: isUnread ? t('Mark_read') : t('Mark_unread'),
onClick: handleToggleRead,
},
canFavorite && {
id: 'toggleFavorite',
icon: isFavorite ? 'star-filled' : 'star',
content: isFavorite ? t('Unfavorite') : t('Favorite'),
onClick: handleToggleFavorite,
},
canLeave && {
id: 'leaveRoom',
icon: 'sign-out',
content: t('Leave_room'),
onClick: handleLeave,
},
];
return options.filter(Boolean) as GenericMenuItemProps[];
}, [
hideDefaultOptions,
subscription,
isOmnichannelRoom,
t,
handleHide,
isUnread,
handleToggleRead,
canFavorite,
isFavorite,
handleToggleFavorite,
canLeave,
handleLeave,
]);
const notificationsMenuOptions = useMemo<GenericMenuItemProps[]>(() => {
if (!subscription || hideDefaultOptions || isOmnichannelRoom || !href) {
return [];
}
const options: GenericMenuItemProps[] = [
{
id: 'turnOnNotifications',
icon: isNotificationEnabled ? 'bell' : 'bell-off',
content: isNotificationEnabled ? t('Turn_OFF') : t('Turn_ON'),
onClick: handleToggleNotification,
},
{
id: 'notifications',
icon: 'customize',
content: t('Preferences'),
onClick: () => router.navigate(`${href}/push-notifications` as LocationPathname),
},
];
return options.filter(Boolean);
}, [hideDefaultOptions, isNotificationEnabled, subscription, t, router, href, handleToggleNotification, isOmnichannelRoom]);
if (isOmnichannelRoom) {
return [
...(roomMenuOptions.length > 0 ? [{ title: '', items: roomMenuOptions }] : []),
...(prioritiesMenu.length > 0 ? [{ title: t('Priorities'), items: prioritiesMenu }] : []),
];
}
return [
...(roomMenuOptions.length > 0 ? [{ title: '', items: roomMenuOptions }] : []),
...(notificationsMenuOptions.length > 0 ? [{ title: t('Notifications'), items: notificationsMenuOptions }] : []),
];
};

View File

@@ -0,0 +1 @@
export { default } from './SidePanelRouter';

View File

@@ -0,0 +1,80 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { SidebarItemIcon } from '@rocket.chat/fuselage';
import { escapeHTML } from '@rocket.chat/tools';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import { useUserId } from '@rocket.chat/ui-contexts';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import SidePanelOmnichannelBadges from './SidePanelOmnichannelBadges';
import { OmnichannelRoomIcon } from '../../../../components/RoomIcon/OmnichannelRoomIcon';
import type { LivechatInquiryLocalRecord } from '../../../../hooks/useLivechatInquiryStore';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
import { isIOsDevice } from '../../../../lib/utils/isIOsDevice';
import { normalizeMessagePreview } from '../../../../lib/utils/normalizeMessagePreview/normalizeMessagePreview';
import { useOmnichannelPriorities } from '../../../omnichannel/hooks/useOmnichannelPriorities';
import SidePanelItem from '../SidepanelItem';
import RoomMenu from '../SidepanelItem/RoomMenu';
export type InquireSidePanelItemProps = {
room: LivechatInquiryLocalRecord;
openedRoom?: string;
};
const InquireSidePanelItem = ({ room, openedRoom, ...props }: InquireSidePanelItemProps) => {
const { t } = useTranslation();
const isAnonymous = !useUserId();
const highlighted = Boolean(room.alert);
const { alert, rid, t: type } = room;
const time = 'lastMessage' in room ? room.lastMessage?.ts : undefined;
const message =
room.lastMessage &&
`${escapeHTML(room.lastMessage.u.name || room.lastMessage.u.username)}: ${normalizeMessagePreview(room.lastMessage, t)}`;
const title = roomCoordinator.getRoomName(room.t, room) || '';
const href = roomCoordinator.getRouteLink(room.t, room) || '';
const badges = <>{isOmnichannelRoom(room) && <SidePanelOmnichannelBadges room={room} />}</>;
const isQueued = room.status === 'queued';
const { enabled: isPriorityEnabled } = useOmnichannelPriorities();
const menu =
!isIOsDevice && !isAnonymous && (!isQueued || (isQueued && isPriorityEnabled)) ? (
<RoomMenu
alert={alert}
rid={rid}
roomOpen={rid === openedRoom}
type={type}
name={title}
hideDefaultOptions={isQueued}
href={href || undefined}
/>
) : undefined;
return (
<SidePanelItem
href={href}
selected={rid === openedRoom}
title={title}
avatar={<RoomAvatar size='x20' room={{ ...room, _id: room.rid || room._id, type: room.t }} />}
icon={
room.source && (
<SidebarItemIcon
highlighted={highlighted}
icon={<OmnichannelRoomIcon placement='sidebar' source={room.source} status={room.v.status} size='x20' />}
/>
)
}
unread={highlighted}
time={time}
subtitle={message ? <span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: message }} /> : null}
badges={badges}
menu={menu}
{...props}
/>
);
};
export default memo(InquireSidePanelItem);

View File

@@ -0,0 +1,20 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import SidePanelPriorityTag from './SidePanelPriorityTag';
import RoomActivityIcon from '../../../../omnichannel/components/RoomActivityIcon';
import { useOmnichannelPriorities } from '../../../../omnichannel/hooks/useOmnichannelPriorities';
export type SidePanelOmnichannelBadgesProps = { room: IOmnichannelRoom };
const SidePanelOmnichannelBadges = ({ room }: SidePanelOmnichannelBadgesProps) => {
const { enabled: isPriorityEnabled } = useOmnichannelPriorities();
return (
<>
{isPriorityEnabled ? <SidePanelPriorityTag priorityWeight={room.priorityWeight} /> : null}
<RoomActivityIcon room={room} />
</>
);
};
export default SidePanelOmnichannelBadges;

View File

@@ -0,0 +1,22 @@
import type { LivechatPriorityWeight } from '@rocket.chat/core-typings';
import { Icon, Tag } from '@rocket.chat/fuselage';
import { useOmnichannelPrioritiesConfig } from '../../../../omnichannel/hooks/useOmnichannelPrioritiesConfig';
export type SidePanelPriorityTagProps = { priorityWeight: LivechatPriorityWeight };
const SidePanelPriorityTag = ({ priorityWeight }: SidePanelPriorityTagProps) => {
const prioritiesConfig = useOmnichannelPrioritiesConfig(priorityWeight, false);
if (!prioritiesConfig?.iconName) {
return null;
}
return (
<Tag icon={<Icon marginInlineEnd={4} size='x12' name={prioritiesConfig?.iconName} />} variant={prioritiesConfig?.variant}>
{prioritiesConfig?.name}
</Tag>
);
};
export default SidePanelPriorityTag;

View File

@@ -0,0 +1 @@
export { default } from './SidePanelOmnichannelBadges';

View File

@@ -0,0 +1,22 @@
import { useTranslation } from 'react-i18next';
import { sidePanelFiltersConfig, useSidePanelRoomsListTab, useUnreadOnlyToggle } from '../../../contexts/RoomsNavigationContext';
import SidePanel from '../../SidePanel';
const SidePanelInProgress = () => {
const { t } = useTranslation();
const rooms = useSidePanelRoomsListTab('inProgress');
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
return (
<SidePanel
title={t(sidePanelFiltersConfig.inProgress.title)}
currentTab='inProgress'
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelInProgress;

View File

@@ -0,0 +1,36 @@
import { usePermission } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
import {
sidePanelFiltersConfig,
useRedirectToDefaultTab,
useSidePanelQueueListTab,
useUnreadOnlyToggle,
} from '../../../contexts/RoomsNavigationContext';
import SidePanelInquiry from '../../SidePanelInquiry';
const SidePanelQueue = () => {
const { t } = useTranslation();
const canViewOmnichannelQueue = usePermission('view-livechat-queue');
const rooms = useSidePanelQueueListTab();
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
useRedirectToDefaultTab(!canViewOmnichannelQueue);
if (!canViewOmnichannelQueue) {
return null;
}
return (
<SidePanelInquiry
title={t(sidePanelFiltersConfig.queue.title)}
currentTab='queue'
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelQueue;

View File

@@ -0,0 +1,35 @@
import { useTranslation } from 'react-i18next';
import { useHasLicenseModule } from '../../../../../hooks/useHasLicenseModule';
import {
sidePanelFiltersConfig,
useRedirectToDefaultTab,
useSidePanelRoomsListTab,
useUnreadOnlyToggle,
} from '../../../contexts/RoomsNavigationContext';
import SidePanel from '../../SidePanel';
const SidePanelOnHold = () => {
const { t } = useTranslation();
const rooms = useSidePanelRoomsListTab('onHold');
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
const { data: hasEEModule = false } = useHasLicenseModule('livechat-enterprise');
useRedirectToDefaultTab(!hasEEModule);
if (!hasEEModule) {
return null;
}
return (
<SidePanel
title={t(sidePanelFiltersConfig.onHold.title)}
currentTab='onHold'
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelOnHold;

View File

@@ -0,0 +1,22 @@
import { useTranslation } from 'react-i18next';
import { sidePanelFiltersConfig, useSidePanelRoomsListTab, useUnreadOnlyToggle } from '../../contexts/RoomsNavigationContext';
import SidePanel from '../SidePanel';
const SidePanelAll = () => {
const { t } = useTranslation();
const rooms = useSidePanelRoomsListTab('all');
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
return (
<SidePanel
title={t(sidePanelFiltersConfig.all.title)}
currentTab='all'
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelAll;

View File

@@ -0,0 +1,28 @@
import type { ISubscription } from '@rocket.chat/core-typings';
import { useUserDisplayName } from '@rocket.chat/ui-client';
import { useUnreadOnlyToggle } from '../../contexts/RoomsNavigationContext';
import SidePanel from '../SidePanel';
import { useChannelsChildrenList } from '../hooks/useChannelsChildrenList';
export type SidePanelChannelsProps = { parentRid: string; subscription: ISubscription };
const SidePanelChannels = ({ parentRid, subscription }: SidePanelChannelsProps) => {
const isDirectSubscription = subscription?.t === 'd';
const username = useUserDisplayName({ name: subscription?.fname, username: subscription?.name });
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
const rooms = useChannelsChildrenList(parentRid, unreadOnly);
return (
<SidePanel
title={(isDirectSubscription ? username : subscription?.fname) || subscription.name}
currentTab={isDirectSubscription ? 'directMessages' : 'channels'}
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelChannels;

View File

@@ -0,0 +1,34 @@
import { useSetting } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
import {
sidePanelFiltersConfig,
useRedirectToDefaultTab,
useSidePanelRoomsListTab,
useUnreadOnlyToggle,
} from '../../contexts/RoomsNavigationContext';
import SidePanel from '../SidePanel';
const SidePanelDiscussions = () => {
const { t } = useTranslation();
const rooms = useSidePanelRoomsListTab('discussions');
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
const isDiscussionEnabled = useSetting('Discussion_enabled');
useRedirectToDefaultTab(!isDiscussionEnabled);
if (!isDiscussionEnabled) {
return null;
}
return (
<SidePanel
title={t(sidePanelFiltersConfig.discussions.title)}
currentTab='discussions'
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelDiscussions;

View File

@@ -0,0 +1,22 @@
import { useTranslation } from 'react-i18next';
import { sidePanelFiltersConfig, useSidePanelRoomsListTab, useUnreadOnlyToggle } from '../../contexts/RoomsNavigationContext';
import SidePanel from '../SidePanel';
const SidePanelFavorites = () => {
const { t } = useTranslation();
const rooms = useSidePanelRoomsListTab('favorites');
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
return (
<SidePanel
title={t(sidePanelFiltersConfig.favorites.title)}
currentTab='favorites'
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelFavorites;

View File

@@ -0,0 +1,28 @@
import { useUserSubscription } from '@rocket.chat/ui-contexts';
import SidePanelAll from './SidePanelAll';
import SidePanelChannels from './SidePanelChannels';
import SidePanelTeams from './SidePanelTeams';
import { withErrorBoundary } from '../../../../components/withErrorBoundary';
import { useSidePanelFilter } from '../../contexts/RoomsNavigationContext';
export type SidePanelRoomsProps = { parentRid: string };
const SidePanelRooms = ({ parentRid }: SidePanelRoomsProps) => {
const [currentTab] = useSidePanelFilter();
const subscription = useUserSubscription(parentRid);
if (!subscription) {
return <SidePanelAll />;
}
switch (currentTab) {
case 'teams':
return <SidePanelTeams parentRid={parentRid} subscription={subscription} />;
case 'channels':
case 'directMessages':
return <SidePanelChannels parentRid={parentRid} subscription={subscription} />;
}
};
export default withErrorBoundary(SidePanelRooms);

View File

@@ -0,0 +1,24 @@
import type { ISubscription } from '@rocket.chat/core-typings';
import { useUnreadOnlyToggle } from '../../contexts/RoomsNavigationContext';
import SidePanel from '../SidePanel';
import { useChannelsChildrenList } from '../hooks/useChannelsChildrenList';
export type SidePanelTeamsProps = { parentRid: string; subscription: ISubscription };
const SidePanelTeams = ({ parentRid, subscription }: SidePanelTeamsProps) => {
const [unreadOnly, toggleUnreadOnly] = useUnreadOnlyToggle();
const rooms = useChannelsChildrenList(parentRid, unreadOnly, subscription?.teamId);
return (
<SidePanel
title={subscription?.fname || subscription?.name}
currentTab='teams'
unreadOnly={unreadOnly}
toggleUnreadOnly={toggleUnreadOnly}
rooms={rooms}
/>
);
};
export default SidePanelTeams;