Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: RoomSidepanel #33225

Merged
merged 14 commits into from
Sep 19, 2024
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
Loading