Skip to content

Commit

Permalink
Chore: Convert apps/meteor/client/sidebar/search (#25754)
Browse files Browse the repository at this point in the history
<!-- This is a pull request template, you do not need to uncomment or remove the comments, they won't show up in the PR text. -->

<!-- Your Pull Request name should start with one of the following tags
  [NEW] For new features
  [IMPROVE] For an improvement (performance or little improvements) in existing features
  [FIX] For bug fixes that affect the end-user
  [BREAK] For pull requests including breaking changes
  Chore: For small tasks
  Doc: For documentation
-->

<!-- Checklist!!! If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. 
  - I have read the Contributing Guide - https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/CONTRIBUTING.md#contributing-to-rocketchat doc
  - I have signed the CLA - https://cla-assistant.io/RocketChat/Rocket.Chat
  - Lint and unit tests pass locally with my changes
  - I have added tests that prove my fix is effective or that my feature works (if applicable)
  - I have added necessary documentation (if applicable)
  - Any dependent changes have been merged and published in downstream modules
-->

## Proposed changes (including videos or screenshots)
<!-- CHANGELOG -->
<!--
  Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
  If it fixes a bug or resolves a feature request, be sure to link to that issue below.
  This description will appear in the release notes if we accept the contribution.
-->

<!-- END CHANGELOG -->

## Issue(s)
<!-- Link the issues being closed by or related to this PR. For example, you can use #594 if this PR closes issue number 594 -->

## Steps to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->

## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->
  • Loading branch information
juliajforesti committed Jun 17, 2022
1 parent b46cb69 commit 92e3230
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 70 deletions.
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

0 comments on commit 92e3230

Please sign in to comment.