feat: `RoomSidepanel` (#33225)
Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com>pull/33179/head
parent
40339749b3
commit
12d6307998
@ -0,0 +1,10 @@ |
||||
--- |
||||
'@rocket.chat/core-services': minor |
||||
'@rocket.chat/model-typings': minor |
||||
'@rocket.chat/core-typings': minor |
||||
'@rocket.chat/rest-typings': minor |
||||
'@rocket.chat/ui-client': minor |
||||
'@rocket.chat/meteor': minor |
||||
--- |
||||
|
||||
Implemented new feature preview for Sidepanel |
||||
@ -0,0 +1,10 @@ |
||||
import { FeaturePreview } from '@rocket.chat/ui-client'; |
||||
import type { ReactElement } from 'react'; |
||||
import React from 'react'; |
||||
|
||||
import { useSidePanelNavigationScreenSize } from '../hooks/useSidePanelNavigation'; |
||||
|
||||
export const FeaturePreviewSidePanelNavigation = ({ children }: { children: ReactElement[] }) => { |
||||
const disabled = !useSidePanelNavigationScreenSize(); |
||||
return <FeaturePreview feature='sidepanelNavigation' disabled={disabled} children={children} />; |
||||
}; |
||||
@ -0,0 +1,14 @@ |
||||
import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; |
||||
import { useFeaturePreview } from '@rocket.chat/ui-client'; |
||||
|
||||
export const useSidePanelNavigation = () => { |
||||
const isSidepanelFeatureEnabled = useFeaturePreview('sidepanelNavigation'); |
||||
// ["xs", "sm", "md", "lg", "xl", xxl"]
|
||||
return useSidePanelNavigationScreenSize() && isSidepanelFeatureEnabled; |
||||
}; |
||||
|
||||
export const useSidePanelNavigationScreenSize = () => { |
||||
const breakpoints = useBreakpoints(); |
||||
// ["xs", "sm", "md", "lg", "xl", xxl"]
|
||||
return breakpoints.includes('lg'); |
||||
}; |
||||
@ -0,0 +1,66 @@ |
||||
/* eslint-disable react/no-multi-comp */ |
||||
import { Box, Sidepanel, SidepanelListItem } from '@rocket.chat/fuselage'; |
||||
import { useUserPreference } from '@rocket.chat/ui-contexts'; |
||||
import React, { memo } from 'react'; |
||||
import { Virtuoso } from 'react-virtuoso'; |
||||
|
||||
import { VirtuosoScrollbars } from '../../../components/CustomScrollbars'; |
||||
import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; |
||||
import { useOpenedRoom, useSecondLevelOpenedRoom } from '../../../lib/RoomManager'; |
||||
import RoomSidepanelListWrapper from './RoomSidepanelListWrapper'; |
||||
import RoomSidepanelLoading from './RoomSidepanelLoading'; |
||||
import RoomSidepanelItem from './SidepanelItem'; |
||||
import { useTeamsListChildrenUpdate } from './hooks/useTeamslistChildren'; |
||||
|
||||
const RoomSidepanel = () => { |
||||
const parentRid = useOpenedRoom(); |
||||
const secondLevelOpenedRoom = useSecondLevelOpenedRoom() ?? parentRid; |
||||
|
||||
if (!parentRid || !secondLevelOpenedRoom) { |
||||
return null; |
||||
} |
||||
|
||||
return <RoomSidepanelWithData parentRid={parentRid} openedRoom={secondLevelOpenedRoom} />; |
||||
}; |
||||
|
||||
const RoomSidepanelWithData = ({ parentRid, openedRoom }: { parentRid: string; openedRoom: string }) => { |
||||
const sidebarViewMode = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode'); |
||||
|
||||
const roomInfo = useRoomInfoEndpoint(parentRid); |
||||
const sidepanelItems = roomInfo.data?.room?.sidepanel?.items || roomInfo.data?.parent?.sidepanel?.items; |
||||
|
||||
const result = useTeamsListChildrenUpdate( |
||||
parentRid, |
||||
!roomInfo.data ? null : roomInfo.data.room?.teamId, |
||||
// eslint-disable-next-line no-nested-ternary
|
||||
!sidepanelItems ? null : sidepanelItems?.length === 1 ? sidepanelItems[0] : undefined, |
||||
); |
||||
if (roomInfo.isSuccess && !roomInfo.data.room?.sidepanel && !roomInfo.data.parent?.sidepanel) { |
||||
return null; |
||||
} |
||||
|
||||
if (roomInfo.isLoading || (roomInfo.isSuccess && result.isLoading)) { |
||||
return <RoomSidepanelLoading />; |
||||
} |
||||
|
||||
if (!result.isSuccess || !roomInfo.isSuccess) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<Sidepanel> |
||||
<Box pb={8} h='full'> |
||||
<Virtuoso |
||||
totalCount={result.data.data.length} |
||||
data={result.data.data} |
||||
components={{ Item: SidepanelListItem, List: RoomSidepanelListWrapper, Scroller: VirtuosoScrollbars }} |
||||
itemContent={(_, data) => ( |
||||
<RoomSidepanelItem openedRoom={openedRoom} room={data} parentRid={parentRid} viewMode={sidebarViewMode} /> |
||||
)} |
||||
/> |
||||
</Box> |
||||
</Sidepanel> |
||||
); |
||||
}; |
||||
|
||||
export default memo(RoomSidepanel); |
||||
@ -0,0 +1,19 @@ |
||||
import { SidepanelList } from '@rocket.chat/fuselage'; |
||||
import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import type { ForwardedRef, HTMLAttributes } from 'react'; |
||||
import React, { forwardRef } from 'react'; |
||||
|
||||
import { useSidebarListNavigation } from '../../../sidebar/RoomList/useSidebarListNavigation'; |
||||
|
||||
type RoomListWrapperProps = HTMLAttributes<HTMLDivElement>; |
||||
|
||||
const RoomSidepanelListWrapper = forwardRef(function RoomListWrapper(props: RoomListWrapperProps, ref: ForwardedRef<HTMLDivElement>) { |
||||
const t = useTranslation(); |
||||
const { sidebarListRef } = useSidebarListNavigation(); |
||||
const mergedRefs = useMergedRefs(ref, sidebarListRef); |
||||
|
||||
return <SidepanelList aria-label={t('Channels')} ref={mergedRefs} {...props} />; |
||||
}); |
||||
|
||||
export default RoomSidepanelListWrapper; |
||||
@ -0,0 +1,20 @@ |
||||
import { SidebarV2Item as SidebarItem, Sidepanel, SidepanelList, Skeleton } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
const RoomSidepanelLoading = () => ( |
||||
<Sidepanel> |
||||
<SidepanelList> |
||||
<SidebarItem> |
||||
<Skeleton w='full' /> |
||||
</SidebarItem> |
||||
<SidebarItem> |
||||
<Skeleton w='full' /> |
||||
</SidebarItem> |
||||
<SidebarItem> |
||||
<Skeleton w='full' /> |
||||
</SidebarItem> |
||||
</SidepanelList> |
||||
</Sidepanel> |
||||
); |
||||
|
||||
export default RoomSidepanelLoading; |
||||
@ -0,0 +1,29 @@ |
||||
import type { IRoom, ISubscription, Serialized } from '@rocket.chat/core-typings'; |
||||
import { useUserSubscription } from '@rocket.chat/ui-contexts'; |
||||
import React, { memo } from 'react'; |
||||
|
||||
import { goToRoomById } from '../../../../lib/utils/goToRoomById'; |
||||
import { useTemplateByViewMode } from '../../../../sidebarv2/hooks/useTemplateByViewMode'; |
||||
import { useItemData } from '../hooks/useItemData'; |
||||
|
||||
export type RoomSidepanelItemProps = { |
||||
openedRoom?: string; |
||||
room: Serialized<IRoom>; |
||||
parentRid: string; |
||||
viewMode?: 'extended' | 'medium' | 'condensed'; |
||||
}; |
||||
|
||||
const RoomSidepanelItem = ({ room, openedRoom, viewMode }: RoomSidepanelItemProps) => { |
||||
const SidepanelItem = useTemplateByViewMode(); |
||||
const subscription = useUserSubscription(room._id); |
||||
|
||||
const itemData = useItemData({ ...room, ...subscription } as ISubscription & IRoom, { viewMode, openedRoom }); // as any because of divergent and overlaping timestamp types in subs and room (type Date vs type string)
|
||||
|
||||
if (!subscription) { |
||||
return <SidepanelItem onClick={goToRoomById} is='a' {...itemData} />; |
||||
} |
||||
|
||||
return <SidepanelItem onClick={goToRoomById} {...itemData} />; |
||||
}; |
||||
|
||||
export default memo(RoomSidepanelItem); |
||||
@ -0,0 +1 @@ |
||||
export { default } from './RoomSidepanelItem'; |
||||
@ -0,0 +1,68 @@ |
||||
import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; |
||||
import { SidebarV2ItemBadge as SidebarItemBadge, SidebarV2ItemIcon as SidebarItemIcon } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { RoomIcon } from '../../../../components/RoomIcon'; |
||||
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; |
||||
import { getBadgeTitle, getMessage } from '../../../../sidebarv2/RoomList/SidebarItemTemplateWithData'; |
||||
import { useAvatarTemplate } from '../../../../sidebarv2/hooks/useAvatarTemplate'; |
||||
|
||||
export const useItemData = ( |
||||
room: ISubscription & IRoom, |
||||
{ openedRoom, viewMode }: { openedRoom: string | undefined; viewMode?: 'extended' | 'medium' | 'condensed' }, |
||||
) => { |
||||
const t = useTranslation(); |
||||
const AvatarTemplate = useAvatarTemplate(); |
||||
|
||||
const highlighted = Boolean(!room.hideUnreadStatus && (room.alert || room.unread)); |
||||
|
||||
const icon = useMemo( |
||||
() => <SidebarItemIcon highlighted={highlighted} icon={<RoomIcon room={room} placement='sidebar' size='x20' />} />, |
||||
[highlighted, room], |
||||
); |
||||
const time = 'lastMessage' in room ? room.lastMessage?.ts : undefined; |
||||
const message = viewMode === 'extended' && getMessage(room, room.lastMessage, t); |
||||
|
||||
const threadUnread = Number(room.tunread?.length) > 0; |
||||
const isUnread = room.unread > 0 || threadUnread; |
||||
const showBadge = |
||||
!room.hideUnreadStatus || (!room.hideMentionStatus && (Boolean(room.userMentions) || Number(room.tunreadUser?.length) > 0)); |
||||
const badgeTitle = getBadgeTitle(room.userMentions, Number(room.tunread?.length), room.groupMentions, room.unread, t); |
||||
const variant = |
||||
((room.userMentions || room.tunreadUser?.length) && 'danger') || |
||||
(threadUnread && 'primary') || |
||||
(room.groupMentions && 'warning') || |
||||
'secondary'; |
||||
|
||||
const badges = useMemo( |
||||
() => ( |
||||
<> |
||||
{showBadge && isUnread && ( |
||||
<SidebarItemBadge variant={variant} title={badgeTitle}> |
||||
{room.unread + (room.tunread?.length || 0)} |
||||
</SidebarItemBadge> |
||||
)} |
||||
</> |
||||
), |
||||
[badgeTitle, isUnread, room.tunread?.length, room.unread, showBadge, variant], |
||||
); |
||||
|
||||
const itemData = useMemo( |
||||
() => ({ |
||||
unread: highlighted, |
||||
selected: room.rid === openedRoom, |
||||
t, |
||||
href: roomCoordinator.getRouteLink(room.t, room) || '', |
||||
title: roomCoordinator.getRoomName(room.t, room) || '', |
||||
icon, |
||||
time, |
||||
badges, |
||||
avatar: AvatarTemplate && <AvatarTemplate {...room} />, |
||||
subtitle: message, |
||||
}), |
||||
[AvatarTemplate, badges, highlighted, icon, message, openedRoom, room, t, time], |
||||
); |
||||
|
||||
return itemData; |
||||
}; |
||||
@ -0,0 +1,106 @@ |
||||
import type { IRoom } from '@rocket.chat/core-typings'; |
||||
import { useEndpoint } from '@rocket.chat/ui-contexts'; |
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; |
||||
import { useEffect, useMemo } from 'react'; |
||||
|
||||
import { ChatRoom } from '../../../../../app/models/client'; |
||||
|
||||
const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { |
||||
if (!a.lm) { |
||||
return 1; |
||||
} |
||||
if (!b.lm) { |
||||
return -1; |
||||
} |
||||
return new Date(b.lm).toUTCString().localeCompare(new Date(a.lm).toUTCString()); |
||||
}; |
||||
|
||||
export const useTeamsListChildrenUpdate = ( |
||||
parentRid: string, |
||||
teamId?: string | null, |
||||
sidepanelItems?: 'channels' | 'discussions' | null, |
||||
) => { |
||||
const queryClient = useQueryClient(); |
||||
|
||||
const query = useMemo(() => { |
||||
const query: Parameters<typeof ChatRoom.find>[0] = { |
||||
$or: [ |
||||
{ |
||||
_id: parentRid, |
||||
}, |
||||
{ |
||||
prid: parentRid, |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
if (teamId && query.$or) { |
||||
query.$or.push({ |
||||
teamId, |
||||
}); |
||||
} |
||||
return query; |
||||
}, [parentRid, teamId]); |
||||
|
||||
const teamList = useEndpoint('GET', '/v1/teams.listChildren'); |
||||
|
||||
const listRoomsAndDiscussions = useEndpoint('GET', '/v1/teams.listChildren'); |
||||
const result = useQuery({ |
||||
queryKey: ['sidepanel', 'list', parentRid, sidepanelItems], |
||||
queryFn: () => |
||||
listRoomsAndDiscussions({ |
||||
roomId: parentRid, |
||||
sort: JSON.stringify({ lm: -1 }), |
||||
type: sidepanelItems || undefined, |
||||
}), |
||||
enabled: sidepanelItems !== null && teamId !== null, |
||||
refetchInterval: 5 * 60 * 1000, |
||||
keepPreviousData: true, |
||||
}); |
||||
|
||||
const { mutate: update } = useMutation({ |
||||
mutationFn: async (params?: { action: 'add' | 'remove' | 'update'; data: IRoom }) => { |
||||
queryClient.setQueryData(['sidepanel', 'list', parentRid, sidepanelItems], (data: Awaited<ReturnType<typeof teamList>> | void) => { |
||||
if (!data) { |
||||
return; |
||||
} |
||||
|
||||
if (params?.action === 'add') { |
||||
data.data = [JSON.parse(JSON.stringify(params.data)), ...data.data].sort(sortRoomByLastMessage); |
||||
} |
||||
|
||||
if (params?.action === 'remove') { |
||||
data.data = data.data.filter((item) => item._id !== params.data?._id); |
||||
} |
||||
|
||||
if (params?.action === 'update') { |
||||
data.data = data.data |
||||
.map((item) => (item._id === params.data?._id ? JSON.parse(JSON.stringify(params.data)) : item)) |
||||
.sort(sortRoomByLastMessage); |
||||
} |
||||
|
||||
return { ...data }; |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
const liveQueryHandle = ChatRoom.find(query).observe({ |
||||
added: (item) => { |
||||
queueMicrotask(() => update({ action: 'add', data: item })); |
||||
}, |
||||
changed: (item) => { |
||||
queueMicrotask(() => update({ action: 'update', data: item })); |
||||
}, |
||||
removed: (item) => { |
||||
queueMicrotask(() => update({ action: 'remove', data: item })); |
||||
}, |
||||
}); |
||||
|
||||
return () => { |
||||
liveQueryHandle.stop(); |
||||
}; |
||||
}, [update, query]); |
||||
|
||||
return result; |
||||
}; |
||||
@ -0,0 +1 @@ |
||||
export { default } from './RoomSidepanel'; |
||||
Loading…
Reference in new issue