Skip to content

Commit

Permalink
feat: RoomSidepanel (#33225)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com>
  • Loading branch information
juliajforesti and ggazzo committed Sep 19, 2024
1 parent 4033974 commit 12d6307
Show file tree
Hide file tree
Showing 28 changed files with 689 additions and 58 deletions.
10 changes: 10 additions & 0 deletions .changeset/witty-lemons-type.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ API.v1.addRoute(
const discussionParent =
room.prid &&
(await Rooms.findOneById<Pick<IRoom, 'name' | 'fname' | 't' | 'prid' | 'u'>>(room.prid, {
projection: { name: 1, fname: 1, t: 1, prid: 1, u: 1 },
projection: { name: 1, fname: 1, t: 1, prid: 1, u: 1, sidepanel: 1 },
}));
const { team, parentRoom } = await Team.getRoomInfo(room);
const parent = discussionParent || parentRoom;
Expand Down
Original file line number Diff line number Diff line change
@@ -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} />;
};
4 changes: 3 additions & 1 deletion apps/meteor/client/hooks/useRoomInfoEndpoint.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { OperationResult } from '@rocket.chat/rest-typings';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useEndpoint, useUserId } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { minutesToMilliseconds } from 'date-fns';
import type { Meteor } from 'meteor/meteor';

export const useRoomInfoEndpoint = (rid: IRoom['_id']): UseQueryResult<OperationResult<'GET', '/v1/rooms.info'>> => {
const getRoomInfo = useEndpoint('GET', '/v1/rooms.info');
const uid = useUserId();
return useQuery(['/v1/rooms.info', rid], () => getRoomInfo({ roomId: rid }), {
cacheTime: minutesToMilliseconds(15),
staleTime: minutesToMilliseconds(5),
Expand All @@ -17,5 +18,6 @@ export const useRoomInfoEndpoint = (rid: IRoom['_id']): UseQueryResult<Operation
}
return true;
},
enabled: !!uid,
});
};
14 changes: 14 additions & 0 deletions apps/meteor/client/hooks/useSidePanelNavigation.ts
Original file line number Diff line number Diff line change
@@ -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');
};
28 changes: 26 additions & 2 deletions apps/meteor/client/lib/RoomManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export const RoomManager = new (class RoomManager extends Emitter<{

private rooms: Map<IRoom['_id'], RoomStore> = new Map();

private parentRid?: IRoom['_id'] | undefined;

constructor() {
super();
debugRoomManager &&
Expand All @@ -78,6 +80,13 @@ export const RoomManager = new (class RoomManager extends Emitter<{
}

get opened(): IRoom['_id'] | undefined {
return this.parentRid ?? this.rid;
}

get openedSecondLevel(): IRoom['_id'] | undefined {
if (!this.parentRid) {
return undefined;
}
return this.rid;
}

Expand Down Expand Up @@ -106,20 +115,28 @@ export const RoomManager = new (class RoomManager extends Emitter<{
this.emit('changed', this.rid);
}

open(rid: IRoom['_id']): void {
private _open(rid: IRoom['_id'], parent?: IRoom['_id']): void {
if (rid === this.rid) {
return;
}

this.back(rid);
if (!this.rooms.has(rid)) {
this.rooms.set(rid, new RoomStore(rid));
}
this.rid = rid;
this.parentRid = parent;
this.emit('opened', this.rid);
this.emit('changed', this.rid);
}

open(rid: IRoom['_id']): void {
this._open(rid);
}

openSecondLevel(parentId: IRoom['_id'], rid: IRoom['_id']): void {
this._open(rid, parentId);
}

getStore(rid: IRoom['_id']): RoomStore | undefined {
return this.rooms.get(rid);
}
Expand All @@ -130,4 +147,11 @@ const subscribeOpenedRoom = [
(): IRoom['_id'] | undefined => RoomManager.opened,
] as const;

const subscribeOpenedSecondLevelRoom = [
(callback: () => void): (() => void) => RoomManager.on('changed', callback),
(): IRoom['_id'] | undefined => RoomManager.openedSecondLevel,
] as const;

export const useOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedRoom);

export const useSecondLevelOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedSecondLevelRoom);
62 changes: 62 additions & 0 deletions apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SidepanelItem } from '@rocket.chat/core-typings';
import {
Box,
Button,
Expand All @@ -16,6 +17,7 @@ import {
AccordionItem,
} from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client';
import {
useEndpoint,
usePermission,
Expand All @@ -40,6 +42,8 @@ type CreateTeamModalInputs = {
encrypted: boolean;
broadcast: boolean;
members?: string[];
showDiscussions?: boolean;
showChannels?: boolean;
};

type CreateTeamModalProps = { onClose: () => void };
Expand All @@ -50,6 +54,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms');
const namesValidation = useSetting('UTF8_Channel_Names_Validation');
const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars');

const dispatchToastMessage = useToastMessageDispatch();
const canCreateTeam = usePermission('create-team');
const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']);
Expand Down Expand Up @@ -94,6 +99,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false,
broadcast: false,
members: [],
showChannels: true,
showDiscussions: true,
},
});

Expand Down Expand Up @@ -123,7 +130,10 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
topic,
broadcast,
encrypted,
showChannels,
showDiscussions,
}: CreateTeamModalInputs): Promise<void> => {
const sidepanelItem = [showChannels && 'channels', showDiscussions && 'discussions'].filter(Boolean) as [SidepanelItem, SidepanelItem?];
const params = {
name,
members,
Expand All @@ -136,6 +146,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
encrypted,
},
},
...((showChannels || showDiscussions) && { sidepanel: { items: sidepanelItem } }),
};

try {
Expand All @@ -157,6 +168,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
const encryptedId = useUniqueId();
const broadcastId = useUniqueId();
const addMembersId = useUniqueId();
const showChannelsId = useUniqueId();
const showDiscussionsId = useUniqueId();

return (
<Modal
Expand Down Expand Up @@ -236,6 +249,55 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
</FieldGroup>
<Accordion>
<AccordionItem title={t('Advanced_settings')}>
<FeaturePreview feature='sidepanelNavigation'>
<FeaturePreviewOff>{null}</FeaturePreviewOff>
<FeaturePreviewOn>
<FieldGroup>
<Box is='h5' fontScale='h5' color='titles-labels'>
{t('Navigation')}
</Box>
<Field>
<FieldRow>
<FieldLabel htmlFor={showChannelsId}>{t('Channels')}</FieldLabel>
<Controller
control={control}
name='showChannels'
render={({ field: { onChange, value, ref } }): ReactElement => (
<ToggleSwitch
aria-describedby={`${showChannelsId}-hint`}
id={showChannelsId}
onChange={onChange}
checked={value}
ref={ref}
/>
)}
/>
</FieldRow>
<FieldDescription id={`${showChannelsId}-hint`}>{t('Show_channels_description')}</FieldDescription>
</Field>

<Field>
<FieldRow>
<FieldLabel htmlFor={showDiscussionsId}>{t('Discussions')}</FieldLabel>
<Controller
control={control}
name='showDiscussions'
render={({ field: { onChange, value, ref } }): ReactElement => (
<ToggleSwitch
aria-describedby={`${showDiscussionsId}-hint`}
id={showDiscussionsId}
onChange={onChange}
checked={value}
ref={ref}
/>
)}
/>
</FieldRow>
<FieldDescription id={`${showDiscussionsId}-hint`}>{t('Show_discussions_description')}</FieldDescription>
</Field>
</FieldGroup>
</FeaturePreviewOn>
</FeaturePreview>
<FieldGroup>
<Box is='h5' fontScale='h5' color='titles-labels'>
{t('Security_and_permissions')}
Expand Down
30 changes: 29 additions & 1 deletion apps/meteor/client/sidebarv2/header/SearchSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@ const wrapperStyle = css`
background-color: ${Palette.surface['surface-sidebar']};
`;

const mobileCheck = function () {
let check = false;
(function (a: string) {
if (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
a,
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
a.substr(0, 4),
)
)
check = true;
})(navigator.userAgent || navigator.vendor || window.opera || '');
return check;
};

const shortcut = ((): string => {
if (navigator.userAgentData?.mobile || mobileCheck()) {
return '';
}
if (window.navigator.platform.toLowerCase().includes('mac')) {
return '(\u2318+K)';
}
return '(Ctrl+K)';
})();

const SearchSection = () => {
const t = useTranslation();
const user = useUser();
Expand Down Expand Up @@ -68,11 +94,13 @@ const SearchSection = () => {
};
}, [handleEscSearch, setFocus]);

const placeholder = [t('Search'), shortcut].filter(Boolean).join(' ');

return (
<Box className={['rcx-sidebar', isDirty && wrapperStyle]} ref={wrapperRef} role='search'>
<SidebarV2Section>
<TextInput
placeholder={t('Search')}
placeholder={placeholder}
{...rest}
ref={mergedRefs}
role='searchbox'
Expand Down
Loading

0 comments on commit 12d6307

Please sign in to comment.