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

Chore: Convert apps/meteor/client/sidebar/search #25754

Merged
merged 16 commits into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ type RoomListRowProps = {
/* @deprecated */
style?: AllHTMLAttributes<HTMLElement>['style'];

selected: boolean;
selected?: boolean;

sidebarViewMode: unknown;
sidebarViewMode?: unknown;
};

function SideBarItemTemplateWithData({
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, any>;
};

const Row = ({ item, data }: RowProps): ReactElement => {
const { t, SideBarItemTemplate, avatarTemplate: AvatarTemplate, useRealName, extended } = data;

if (item.t === 'd' && !item.u) {
Expand All @@ -21,7 +27,6 @@ const Row = ({ item, data }) => {
return (
<SideBarItemTemplateWithData
id={`search-${item._id}`}
tabIndex={-1}
extended={extended}
t={t}
room={item}
Expand Down
16 changes: 0 additions & 16 deletions apps/meteor/client/sidebar/search/ScrollerWithCustomProps.js

This file was deleted.

16 changes: 16 additions & 0 deletions apps/meteor/client/sidebar/search/ScrollerWithCustomProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { forwardRef, ReactElement } from 'react';

import ScrollableContentWrapper from '../../components/ScrollableContentWrapper';

const ScrollerWithCustomProps = forwardRef(function ScrollerWithCustomProps(props, ref: React.Ref<HTMLElement>) {
return (
<ScrollableContentWrapper
{...props}
ref={ref}
renderView={({ style, ...props }): ReactElement => <div {...props} style={{ ...style }} />}
renderTrackHorizontal={(props): ReactElement => <div {...props} style={{ display: 'none' }} className='track-horizontal' />}
/>
);
});

export default ScrollerWithCustomProps;
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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')) {
Expand All @@ -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 === '@';
Expand All @@ -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) {
Expand All @@ -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');

Expand All @@ -76,72 +96,101 @@ 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,
fname: user.name,
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<SetStateAction<string>> } => {
const [value, setValue] = useState(initial);
const onChange = useMutableCallback((e) => {
setValue(e.currentTarget.value);
});
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');
}
};

/**
* @type import('react').ForwardRefExoticComponent<{ onClose: unknown } & import('react').RefAttributes<HTMLElement>>
*/
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<HTMLInputElement>(null);
const autofocus: Ref<HTMLInputElement> = useMergedRefs(useAutoFocus<HTMLInputElement>(), cursorRef);

const listRef = useRef();
const boxRef = useRef();
const listRef = useRef<VirtuosoHandle>(null);
const boxRef = useRef<HTMLDivElement>(null);

const selectedElement = useRef();
const selectedElement: MutableRefObject<HTMLElement | null | undefined> = useRef(null);
const itemIndexRef = useRef(0);

const sidebarViewMode = useUserPreference('sidebarViewMode');
Expand Down Expand Up @@ -175,26 +224,26 @@ 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;
});

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);
}
});

Expand All @@ -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) => {
Expand All @@ -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: () => {
Expand All @@ -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 (
<Box
Expand All @@ -260,7 +309,7 @@ const SearchList = forwardRef(function SearchList({ onClose }, ref) {
`}
ref={ref}
>
<Sidebar.TopBar.Section role='search' is='form'>
<Sidebar.TopBar.Section {...({ role: 'search' } as any)} is='form'>
<TextInput
aria-owns={listId}
data-qa='sidebar-search-input'
Expand Down Expand Up @@ -288,7 +337,7 @@ const SearchList = forwardRef(function SearchList({ onClose }, ref) {
totalCount={items?.length}
data={items}
components={{ Scroller: ScrollerWithCustomProps }}
itemContent={(index, data) => <Row data={itemData} item={data} />}
itemContent={(_, data): ReactElement => <Row data={itemData} item={data} />}
ref={listRef}
/>
</Box>
Expand Down
Loading