From 92e3230788b7517dc6bcebc681b65f16d83a508f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Fri, 17 Jun 2022 15:38:21 -0300 Subject: [PATCH] Chore: Convert apps/meteor/client/sidebar/search (#25754) ## Proposed changes (including videos or screenshots) ## Issue(s) ## Steps to test or reproduce ## Further comments --- .../RoomList/SideBarItemTemplateWithData.tsx | 4 +- .../client/sidebar/search/{Row.js => Row.tsx} | 11 +- .../sidebar/search/ScrollerWithCustomProps.js | 16 -- .../search/ScrollerWithCustomProps.tsx | 16 ++ .../search/{SearchList.js => SearchList.tsx} | 137 ++++++++++++------ .../search/{UserItem.js => UserItem.tsx} | 24 ++- .../definition/externals/meteor/meteor.d.ts | 4 + .../ui-contexts/src/ServerContext/methods.ts | 20 +++ 8 files changed, 162 insertions(+), 70 deletions(-) rename apps/meteor/client/sidebar/search/{Row.js => Row.tsx} (73%) delete mode 100644 apps/meteor/client/sidebar/search/ScrollerWithCustomProps.js create mode 100644 apps/meteor/client/sidebar/search/ScrollerWithCustomProps.tsx rename apps/meteor/client/sidebar/search/{SearchList.js => SearchList.tsx} (60%) rename apps/meteor/client/sidebar/search/{UserItem.js => UserItem.tsx} (59%) diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index 8a01dfce72ce..104a0b97496f 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -66,9 +66,9 @@ type RoomListRowProps = { /* @deprecated */ style?: AllHTMLAttributes['style']; - selected: boolean; + selected?: boolean; - sidebarViewMode: unknown; + sidebarViewMode?: unknown; }; function SideBarItemTemplateWithData({ diff --git a/apps/meteor/client/sidebar/search/Row.js b/apps/meteor/client/sidebar/search/Row.tsx similarity index 73% rename from apps/meteor/client/sidebar/search/Row.js rename to apps/meteor/client/sidebar/search/Row.tsx index c1134cd5e45e..475eae9e2a47 100644 --- a/apps/meteor/client/sidebar/search/Row.js +++ b/apps/meteor/client/sidebar/search/Row.tsx @@ -1,9 +1,15 @@ -import React, { memo } from 'react'; +import { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import React, { memo, ReactElement } from 'react'; import SideBarItemTemplateWithData from '../RoomList/SideBarItemTemplateWithData'; import UserItem from './UserItem'; -const Row = ({ item, data }) => { +type RowProps = { + item: ISubscription & IRoom; + data: Record; +}; + +const Row = ({ item, data }: RowProps): ReactElement => { const { t, SideBarItemTemplate, avatarTemplate: AvatarTemplate, useRealName, extended } = data; if (item.t === 'd' && !item.u) { @@ -21,7 +27,6 @@ const Row = ({ item, data }) => { return (
} - renderTrackHorizontal={(props) =>
} - /> - ); -}); - -export default ScrollerWithCustomProps; diff --git a/apps/meteor/client/sidebar/search/ScrollerWithCustomProps.tsx b/apps/meteor/client/sidebar/search/ScrollerWithCustomProps.tsx new file mode 100644 index 000000000000..3066c0d218e6 --- /dev/null +++ b/apps/meteor/client/sidebar/search/ScrollerWithCustomProps.tsx @@ -0,0 +1,16 @@ +import React, { forwardRef, ReactElement } from 'react'; + +import ScrollableContentWrapper from '../../components/ScrollableContentWrapper'; + +const ScrollerWithCustomProps = forwardRef(function ScrollerWithCustomProps(props, ref: React.Ref) { + return ( +
} + renderTrackHorizontal={(props): ReactElement =>
} + /> + ); +}); + +export default ScrollerWithCustomProps; diff --git a/apps/meteor/client/sidebar/search/SearchList.js b/apps/meteor/client/sidebar/search/SearchList.tsx similarity index 60% rename from apps/meteor/client/sidebar/search/SearchList.js rename to apps/meteor/client/sidebar/search/SearchList.tsx index 33f677ed8b39..80be10d7ac62 100644 --- a/apps/meteor/client/sidebar/search/SearchList.js +++ b/apps/meteor/client/sidebar/search/SearchList.tsx @@ -1,11 +1,31 @@ +import { RoomType } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Sidebar, TextInput, Box, Icon } from '@rocket.chat/fuselage'; -import { useMutableCallback, useDebouncedValue, useStableArray, useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { + useMutableCallback, + useDebouncedValue, + useStableArray, + useAutoFocus, + useUniqueId, + useMergedRefs, +} from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { useUserPreference, useUserSubscriptions, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import React, { forwardRef, useState, useMemo, useEffect, useRef } from 'react'; -import { Virtuoso } from 'react-virtuoso'; +import React, { + forwardRef, + useState, + useMemo, + useEffect, + useRef, + ReactElement, + MutableRefObject, + SetStateAction, + Dispatch, + FormEventHandler, + Ref, +} from 'react'; +import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import tinykeys from 'tinykeys'; import { AsyncStatePhase } from '../../hooks/useAsyncState'; @@ -15,8 +35,8 @@ import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; import Row from './Row'; import ScrollerWithCustomProps from './ScrollerWithCustomProps'; -const shortcut = (() => { - if (!Meteor.Device.isDesktop()) { +const shortcut = ((): string => { + if (!(Meteor as any).Device.isDesktop()) { return ''; } if (window.navigator.platform.toLowerCase().includes('mac')) { @@ -25,9 +45,9 @@ const shortcut = (() => { return '(\u2303+K)'; })(); -const useSpotlight = (filterText = '', usernames) => { +const useSpotlight = (filterText: string, usernames: string[]) => { const expression = /(@|#)?(.*)/i; - const [, mention, name] = filterText.match(expression); + const [, mention, name] = filterText.match(expression) || []; const searchForChannels = mention === '#'; const searchForDMs = mention === '@'; @@ -41,9 +61,10 @@ const useSpotlight = (filterText = '', usernames) => { } return { users: true, rooms: true }; }, [searchForChannels, searchForDMs]); + const args = useMemo(() => [name, usernames, type], [type, name, usernames]); - const { value: data = { users: [], rooms: [] }, phase: status } = useMethodData('spotlight', args); + const { value: data, phase: status } = useMethodData('spotlight', args); return useMemo(() => { if (!data) { @@ -60,11 +81,10 @@ const options = { }, }; -const useSearchItems = (filterText) => { +const useSearchItems = (filterText: string): any => { const expression = /(@|#)?(.*)/i; - const teste = filterText.match(expression); + const [, type, name] = filterText.match(expression) || []; - const [, type, name] = teste; const query = useMemo(() => { const filterRegex = new RegExp(escapeRegExp(name), 'i'); @@ -76,23 +96,36 @@ const useSearchItems = (filterText) => { }; }, [name, type]); - const localRooms = useUserSubscriptions(query, options); + const localRooms: { rid: string; t: RoomType; _id: string; name: string; uids?: string }[] = useUserSubscriptions(query, options); - const usernamesFromClient = useStableArray([...localRooms?.map(({ t, name }) => (t === 'd' ? name : null))].filter(Boolean)); + const usernamesFromClient = useStableArray([...localRooms?.map(({ t, name }) => (t === 'd' ? name : null))].filter(Boolean)) as string[]; const { data: spotlight, status } = useSpotlight(filterText, usernamesFromClient); return useMemo(() => { - const resultsFromServer = []; + const filterUsersUnique = ({ _id }: { _id: string }, index: number, arr: { _id: string }[]): boolean => + index === arr.findIndex((user) => _id === user._id); - const filterUsersUnique = ({ _id }, index, arr) => index === arr.findIndex((user) => _id === user._id); - const roomFilter = (room) => + const roomFilter = (room: { t: string; uids?: string[]; _id: string; name?: string }): boolean => !localRooms.find( - (item) => (room.t === 'd' && room.uids?.length > 1 && room.uids.includes(item._id)) || [item.rid, item._id].includes(room._id), + (item) => + (room.t === 'd' && room.uids && room.uids.length > 1 && room.uids?.includes(item._id)) || [item.rid, item._id].includes(room._id), ); - const usersfilter = (user) => !localRooms.find((room) => room.t === 'd' && room.uids?.length === 2 && room.uids.includes(user._id)); - - const userMap = (user) => ({ + const usersfilter = (user: { _id: string }): boolean => + !localRooms.find((room) => room.t === 'd' && room.uids && room.uids?.length === 2 && room.uids.includes(user._id)); + + const userMap = (user: { + _id: string; + name: string; + username: string; + avatarETag?: string; + }): { + _id: string; + t: string; + name: string; + fname: string; + avatarETag?: string; + } => ({ _id: user._id, t: 'd', name: user.username, @@ -100,17 +133,27 @@ const useSearchItems = (filterText) => { avatarETag: user.avatarETag, }); - const exact = resultsFromServer.filter((item) => [item.usernamame, item.name, item.fname].includes(name)); + type resultsFromServerType = { + _id: string; + t: string; + name: string; + fname?: string; + avatarETag?: string | undefined; + uids?: string[] | undefined; + }[]; + const resultsFromServer: resultsFromServerType = []; resultsFromServer.push(...spotlight.users.filter(filterUsersUnique).filter(usersfilter).map(userMap)); resultsFromServer.push(...spotlight.rooms.filter(roomFilter)); + const exact = resultsFromServer?.filter((item) => [item.name, item.fname].includes(name)); + return { data: Array.from(new Set([...exact, ...localRooms, ...resultsFromServer])), status }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [localRooms, name, spotlight]); }; -const useInput = (initial) => { +const useInput = (initial: string): { value: string; onChange: FormEventHandler; setValue: Dispatch> } => { const [value, setValue] = useState(initial); const onChange = useMutableCallback((e) => { setValue(e.currentTarget.value); @@ -118,12 +161,12 @@ const useInput = (initial) => { return { value, onChange, setValue }; }; -const toggleSelectionState = (next, current, input) => { - input.setAttribute('aria-activedescendant', next.id); - next.setAttribute('aria-selected', true); +const toggleSelectionState = (next: HTMLElement, current: HTMLElement | undefined, input: HTMLElement | undefined): void => { + input?.setAttribute('aria-activedescendant', next.id); + next.setAttribute('aria-selected', 'true'); next.classList.add('rcx-sidebar-item--selected'); if (current) { - current.setAttribute('aria-selected', false); + current.removeAttribute('aria-selected'); current.classList.remove('rcx-sidebar-item--selected'); } }; @@ -131,17 +174,23 @@ const toggleSelectionState = (next, current, input) => { /** * @type import('react').ForwardRefExoticComponent<{ onClose: unknown } & import('react').RefAttributes> */ -const SearchList = forwardRef(function SearchList({ onClose }, ref) { + +type SearchListProps = { + onClose: () => void; +}; + +const SearchList = forwardRef(function SearchList({ onClose }: SearchListProps, ref): ReactElement { const listId = useUniqueId(); const t = useTranslation(); const { setValue: setFilterValue, ...filter } = useInput(''); - const autofocus = useAutoFocus(); + const cursorRef = useRef(null); + const autofocus: Ref = useMergedRefs(useAutoFocus(), cursorRef); - const listRef = useRef(); - const boxRef = useRef(); + const listRef = useRef(null); + const boxRef = useRef(null); - const selectedElement = useRef(); + const selectedElement: MutableRefObject = useRef(null); const itemIndexRef = useRef(0); const sidebarViewMode = useUserPreference('sidebarViewMode'); @@ -175,13 +224,13 @@ const SearchList = forwardRef(function SearchList({ onClose }, ref) { let nextSelectedElement = null; if (dir === 'up') { - nextSelectedElement = selectedElement.current.parentElement.previousSibling.querySelector('a'); + nextSelectedElement = (selectedElement.current?.parentElement?.previousSibling as HTMLElement).querySelector('a'); } else { - nextSelectedElement = selectedElement.current.parentElement.nextSibling.querySelector('a'); + nextSelectedElement = (selectedElement.current?.parentElement?.nextSibling as HTMLElement).querySelector('a'); } if (nextSelectedElement) { - toggleSelectionState(nextSelectedElement, selectedElement.current, autofocus.current); + toggleSelectionState(nextSelectedElement, selectedElement.current || undefined, cursorRef?.current || undefined); return nextSelectedElement; } return selectedElement.current; @@ -189,12 +238,12 @@ const SearchList = forwardRef(function SearchList({ onClose }, ref) { const resetCursor = useMutableCallback(() => { itemIndexRef.current = 0; - listRef.current.scrollToIndex({ index: itemIndexRef.current }); + listRef.current?.scrollToIndex({ index: itemIndexRef.current }); selectedElement.current = boxRef.current?.querySelector('a.rcx-sidebar-item'); if (selectedElement.current) { - toggleSelectionState(selectedElement.current, undefined, autofocus.current); + toggleSelectionState(selectedElement.current, undefined, cursorRef?.current || undefined); } }); @@ -207,10 +256,10 @@ const SearchList = forwardRef(function SearchList({ onClose }, ref) { }, [filterText, resetCursor]); useEffect(() => { - if (!autofocus.current) { + if (!cursorRef?.current) { return; } - const unsubscribe = tinykeys(autofocus.current, { + const unsubscribe = tinykeys(cursorRef?.current, { Escape: (event) => { event.preventDefault(); setFilterValue((value) => { @@ -225,13 +274,13 @@ const SearchList = forwardRef(function SearchList({ onClose }, ref) { ArrowUp: () => { const currentElement = changeSelection('up'); itemIndexRef.current = Math.max(itemIndexRef.current - 1, 0); - listRef.current.scrollToIndex({ index: itemIndexRef.current }); + listRef.current?.scrollToIndex({ index: itemIndexRef.current }); selectedElement.current = currentElement; }, ArrowDown: () => { const currentElement = changeSelection('down'); itemIndexRef.current = Math.min(itemIndexRef.current + 1, items?.length + 1); - listRef.current.scrollToIndex({ index: itemIndexRef.current }); + listRef.current?.scrollToIndex({ index: itemIndexRef.current }); selectedElement.current = currentElement; }, Enter: () => { @@ -240,10 +289,10 @@ const SearchList = forwardRef(function SearchList({ onClose }, ref) { } }, }); - return () => { + return (): void => { unsubscribe(); }; - }, [autofocus, changeSelection, items.length, onClose, resetCursor, setFilterValue]); + }, [cursorRef, changeSelection, items.length, onClose, resetCursor, setFilterValue]); return ( - + } + itemContent={(_, data): ReactElement => } ref={listRef} /> diff --git a/apps/meteor/client/sidebar/search/UserItem.js b/apps/meteor/client/sidebar/search/UserItem.tsx similarity index 59% rename from apps/meteor/client/sidebar/search/UserItem.js rename to apps/meteor/client/sidebar/search/UserItem.tsx index febaa41717c6..21840bcc414a 100644 --- a/apps/meteor/client/sidebar/search/UserItem.js +++ b/apps/meteor/client/sidebar/search/UserItem.tsx @@ -1,13 +1,28 @@ +import { IUser } from '@rocket.chat/core-typings'; import { Sidebar } from '@rocket.chat/fuselage'; -import React, { memo } from 'react'; +import React, { memo, ReactElement } from 'react'; import { ReactiveUserStatus } from '../../components/UserStatus'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; -const UserItem = ({ item, id, style, t, SideBarItemTemplate, AvatarTemplate, useRealName }) => { +type UserItemProps = { + item: { + name?: string; + fname?: string; + _id: IUser['_id']; + t: string; + }; + t: (value: string) => string; + SideBarItemTemplate: any; + AvatarTemplate: any; + id: string; + style?: CSSStyleRule; + useRealName?: boolean; +}; +const UserItem = ({ item, id, style, t, SideBarItemTemplate, AvatarTemplate, useRealName }: UserItemProps): ReactElement => { const title = useRealName ? item.fname || item.name : item.name || item.fname; const icon = ( - + ); @@ -16,14 +31,13 @@ const UserItem = ({ item, id, style, t, SideBarItemTemplate, AvatarTemplate, use return ( } icon={icon} - style={style} /> ); }; diff --git a/apps/meteor/definition/externals/meteor/meteor.d.ts b/apps/meteor/definition/externals/meteor/meteor.d.ts index 696c893c4bca..f797375ed723 100644 --- a/apps/meteor/definition/externals/meteor/meteor.d.ts +++ b/apps/meteor/definition/externals/meteor/meteor.d.ts @@ -12,6 +12,10 @@ declare module 'meteor/meteor' { reason?: string; } + interface Device { + isDesktop: () => boolean; + } + const server: any; const runAsUser: (userId: string, scope: Function) => any; diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index 5cdbb326439c..825febf0e22c 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -127,6 +127,26 @@ export interface ServerMethods { 'livechat:saveAgentInfo': (_id: string, agentData: unknown, agentDepartments: unknown) => unknown; 'autoTranslate.getProviderUiMetadata': () => Record; 'autoTranslate.getSupportedLanguages': (language: string) => ISupportedLanguage[]; + 'spotlight': ( + ...args: ( + | string + | string[] + | { + users: boolean; + rooms: boolean; + } + )[] + ) => { + rooms: { _id: string; name: string; t: string; uids?: string[] }[]; + users: { + _id: string; + status: 'offline' | 'online' | 'busy' | 'away'; + name: string; + username: string; + outside: boolean; + avatarETag?: string; + }[]; + }; } export type ServerMethodName = keyof ServerMethods;