Member drawer filter (#1457)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Thu, 19 Oct 2023 06:43:16 +0000 (17:43 +1100)
committerGitHub <noreply@github.com>
Thu, 19 Oct 2023 06:43:16 +0000 (17:43 +1100)
* save member drawer sort filter in local storage

* render member drawer with key

* improve member search

src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
src/app/hooks/useAsyncSearch.ts
src/app/organisms/room/MembersDrawer.tsx
src/app/organisms/room/Room.tsx
src/app/organisms/room/RoomInput.tsx
src/app/organisms/room/message/MessageEditor.tsx
src/app/state/settings.ts
src/app/utils/room.ts

index a99274a5d94ee66e15592c89404598a692a75d23..8c3d91bf1569a9d642b433a7e3da0161a57f6fc4 100644 (file)
@@ -1,7 +1,7 @@
 import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
 import { Editor } from 'slate';
 import { Avatar, AvatarFallback, AvatarImage, MenuItem, Text, color } from 'folds';
-import { MatrixClient, RoomMember } from 'matrix-js-sdk';
+import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
 
 import { AutocompleteQuery } from './autocompleteQuery';
 import { AutocompleteMenu } from './AutocompleteMenu';
@@ -16,6 +16,7 @@ import { onTabPress } from '../../../utils/keyboard';
 import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
 import { useKeyDown } from '../../../hooks/useKeyDown';
 import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
+import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
 
 type MentionAutoCompleteHandler = (userId: string, name: string) => void;
 
@@ -64,7 +65,7 @@ function UnknownMentionItem({
 }
 
 type UserMentionAutocompleteProps = {
-  roomId: string;
+  room: Room;
   editor: Editor;
   query: AutocompleteQuery<string>;
   requestClose: () => void;
@@ -77,21 +78,19 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = {
   },
 };
 
-const getRoomMemberStr: SearchItemStrGetter<RoomMember> = (roomMember) => [
-  roomMember.name,
-  getMxIdLocalPart(roomMember.userId) ?? roomMember.userId,
-  roomMember.userId,
-];
+const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId;
+const getRoomMemberStr: SearchItemStrGetter<RoomMember> = (m, query) =>
+  getMemberSearchStr(m, query, mxIdToName);
 
 export function UserMentionAutocomplete({
-  roomId,
+  room,
   editor,
   query,
   requestClose,
 }: UserMentionAutocompleteProps) {
   const mx = useMatrixClient();
-  const room = mx.getRoom(roomId);
-  const roomAliasOrId = room?.getCanonicalAlias() || roomId;
+  const roomId: string = room.roomId!;
+  const roomAliasOrId = room.getCanonicalAlias() || roomId;
   const members = useRoomMembers(mx, roomId);
 
   const [result, search, resetSearch] = useAsyncSearch(members, getRoomMemberStr, SEARCH_OPTIONS);
@@ -129,6 +128,9 @@ export function UserMentionAutocomplete({
     });
   });
 
+  const getName = (member: RoomMember) =>
+    getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
+
   return (
     <AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}>
       {query.text === 'room' && (
@@ -155,9 +157,9 @@ export function UserMentionAutocomplete({
               as="button"
               radii="300"
               onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
-                onTabPress(evt, () => handleAutocomplete(roomMember.userId, roomMember.name))
+                onTabPress(evt, () => handleAutocomplete(roomMember.userId, getName(roomMember)))
               }
-              onClick={() => handleAutocomplete(roomMember.userId, roomMember.name)}
+              onClick={() => handleAutocomplete(roomMember.userId, getName(roomMember))}
               after={
                 <Text size="T200" priority="300" truncate>
                   {roomMember.userId}
@@ -166,7 +168,7 @@ export function UserMentionAutocomplete({
               before={
                 <Avatar size="200">
                   {avatarUrl ? (
-                    <AvatarImage src={avatarUrl} alt={roomMember.userId} />
+                    <AvatarImage src={avatarUrl} alt={getName(roomMember)} />
                   ) : (
                     <AvatarFallback
                       style={{
@@ -174,14 +176,14 @@ export function UserMentionAutocomplete({
                         color: color.Secondary.OnContainer,
                       }}
                     >
-                      <Text size="H6">{roomMember.name[0] || roomMember.userId[1]}</Text>
+                      <Text size="H6">{getName(roomMember)[0]}</Text>
                     </AvatarFallback>
                   )}
                 </Avatar>
               }
             >
               <Text style={{ flexGrow: 1 }} size="B400" truncate>
-                {roomMember.name}
+                {getName(roomMember)}
               </Text>
             </MenuItem>
           );
index d0e73e7f8a51b98a13a1aee2850b5388af55b48e..719e0d5618aae42a3b5c9cbbd08fb3fc81240c00 100644 (file)
@@ -17,7 +17,8 @@ export type UseAsyncSearchOptions = AsyncSearchOption & {
 };
 
 export type SearchItemStrGetter<TSearchItem extends object | string | number> = (
-  searchItem: TSearchItem
+  searchItem: TSearchItem,
+  query: string
 ) => string | string[];
 
 export type UseAsyncSearchResult<TSearchItem extends object | string | number> = {
@@ -38,7 +39,7 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
     setResult(undefined);
 
     const handleMatch: MatchHandler<TSearchItem> = (item, query) => {
-      const itemStr = getItemStr(item);
+      const itemStr = getItemStr(item, query);
       if (Array.isArray(itemStr))
         return !!itemStr.find((i) =>
           matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)
index d0bb55d5ddf9c741736f307689a617ef7bedb037..365dc62d678eadd1dba9e310a860d8b8b0e1fe74 100644 (file)
@@ -46,14 +46,20 @@ import {
 } from '../../hooks/useIntersectionObserver';
 import { Membership } from '../../../types/matrix/room';
 import { UseStateProvider } from '../../components/UseStateProvider';
-import { UseAsyncSearchOptions, useAsyncSearch } from '../../hooks/useAsyncSearch';
+import {
+  SearchItemStrGetter,
+  UseAsyncSearchOptions,
+  useAsyncSearch,
+} from '../../hooks/useAsyncSearch';
 import { useDebounce } from '../../hooks/useDebounce';
 import colorMXID from '../../../util/colorMXID';
 import { usePowerLevelTags, PowerLevelTag } from '../../hooks/usePowerLevelTags';
 import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../../state/typingMembers';
 import { TypingIndicator } from '../../components/typing-indicator';
-import { getMemberDisplayName } from '../../utils/room';
+import { getMemberDisplayName, getMemberSearchStr } from '../../utils/room';
 import { getMxIdLocalPart } from '../../utils/matrix';
+import { useSetting } from '../../state/hooks/settings';
+import { settingsAtom } from '../../state/settings';
 
 export const MembershipFilters = {
   filterJoined: (m: RoomMember) => m.membership === Membership.Join,
@@ -159,7 +165,10 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = {
     contain: true,
   },
 };
-const getMemberItemStr = (m: RoomMember) => [m.name, m.userId];
+
+const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId;
+const getRoomMemberStr: SearchItemStrGetter<RoomMember> = (m, query) =>
+  getMemberSearchStr(m, query, mxIdToName);
 
 type MembersDrawerProps = {
   room: Room;
@@ -175,10 +184,12 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
 
   const membershipFilterMenu = useMembershipFilterMenu();
   const sortFilterMenu = useSortFilterMenu();
-  const [filter, setFilter] = useState<MembersFilterOptions>({
-    membershipFilter: membershipFilterMenu[0],
-    sortFilter: sortFilterMenu[0],
-  });
+  const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
+  const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
+
+  const membershipFilter = membershipFilterMenu[membershipFilterIndex] ?? membershipFilterMenu[0];
+  const sortFilter = sortFilterMenu[sortFilterIndex] ?? sortFilterMenu[0];
+
   const [onTop, setOnTop] = useState(true);
 
   const typingMembers = useAtomValue(
@@ -188,15 +199,15 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
   const filteredMembers = useMemo(
     () =>
       members
-        .filter(filter.membershipFilter.filterFn)
-        .sort(filter.sortFilter.filterFn)
+        .filter(membershipFilter.filterFn)
+        .sort(sortFilter.filterFn)
         .sort((a, b) => b.powerLevel - a.powerLevel),
-    [members, filter]
+    [members, membershipFilter, sortFilter]
   );
 
   const [result, search, resetSearch] = useAsyncSearch(
     filteredMembers,
-    getMemberItemStr,
+    getRoomMemberStr,
     SEARCH_OPTIONS
   );
   if (!result && searchInputRef.current?.value) search(searchInputRef.current.value);
@@ -310,18 +321,18 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
                           }}
                         >
                           <Menu style={{ padding: config.space.S100 }}>
-                            {membershipFilterMenu.map((menuItem) => (
+                            {membershipFilterMenu.map((menuItem, index) => (
                               <MenuItem
                                 key={menuItem.name}
                                 variant={
-                                  menuItem.name === filter.membershipFilter.name
+                                  menuItem.name === membershipFilter.name
                                     ? menuItem.color
                                     : 'Surface'
                                 }
-                                aria-pressed={menuItem.name === filter.membershipFilter.name}
+                                aria-pressed={menuItem.name === membershipFilter.name}
                                 radii="300"
                                 onClick={() => {
-                                  setFilter((f) => ({ ...f, membershipFilter: menuItem }));
+                                  setMembershipFilterIndex(index);
                                   setOpen(false);
                                 }}
                               >
@@ -336,12 +347,12 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
                         <Chip
                           ref={anchorRef}
                           onClick={() => setOpen(!open)}
-                          variant={filter.membershipFilter.color}
+                          variant={membershipFilter.color}
                           size="400"
                           radii="300"
                           before={<Icon src={Icons.Filter} size="50" />}
                         >
-                          <Text size="T200">{filter.membershipFilter.name}</Text>
+                          <Text size="T200">{membershipFilter.name}</Text>
                         </Chip>
                       )}
                     </PopOut>
@@ -365,14 +376,14 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
                           }}
                         >
                           <Menu style={{ padding: config.space.S100 }}>
-                            {sortFilterMenu.map((menuItem) => (
+                            {sortFilterMenu.map((menuItem, index) => (
                               <MenuItem
                                 key={menuItem.name}
                                 variant="Surface"
-                                aria-pressed={menuItem.name === filter.sortFilter.name}
+                                aria-pressed={menuItem.name === sortFilter.name}
                                 radii="300"
                                 onClick={() => {
-                                  setFilter((f) => ({ ...f, sortFilter: menuItem }));
+                                  setSortFilterIndex(index);
                                   setOpen(false);
                                 }}
                               >
@@ -392,7 +403,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
                           radii="300"
                           after={<Icon src={Icons.Sort} size="50" />}
                         >
-                          <Text size="T200">{filter.sortFilter.name}</Text>
+                          <Text size="T200">{sortFilter.name}</Text>
                         </Chip>
                       )}
                     </PopOut>
@@ -452,7 +463,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
 
             {!fetchingMembers && !result && processMembers.length === 0 && (
               <Text style={{ padding: config.space.S300 }} align="Center">
-                {`No "${filter.membershipFilter.name}" Members`}
+                {`No "${membershipFilter.name}" Members`}
               </Text>
             )}
 
index 094daadc52b2e91cfd71096a3ae5bcbd81840f18..6158547c3d899c3ff7bb4bc6312ea7fad96e98db 100644 (file)
@@ -37,7 +37,7 @@ export function RoomBaseView({ room, eventId }: RoomBaseViewProps) {
         {screenSize === ScreenSize.Desktop && isDrawer && (
           <>
             <Line variant="Background" direction="Vertical" size="300" />
-            <MembersDrawer room={room} />
+            <MembersDrawer key={room.roomId} room={room} />
           </>
         )}
       </div>
index 81c29b03040b21406a5e2906de369c9af5c58d57..ae12afb05f3c2c09df7eece0d53ddba7574668e2 100644 (file)
@@ -443,7 +443,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
         )}
         {autocompleteQuery?.prefix === AutocompletePrefix.UserMention && (
           <UserMentionAutocomplete
-            roomId={roomId}
+            room={room}
             editor={editor}
             query={autocompleteQuery}
             requestClose={handleCloseAutocomplete}
index 6d1c492863036aac9ec7c3d7ceee4e073e327216..f38cfbef74103473dfd6a0f5cbadaba505113f90 100644 (file)
@@ -193,7 +193,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
         )}
         {autocompleteQuery?.prefix === AutocompletePrefix.UserMention && (
           <UserMentionAutocomplete
-            roomId={roomId}
+            room={room}
             editor={editor}
             query={autocompleteQuery}
             requestClose={handleCloseAutocomplete}
index f59951d1c576b4b6390a8bbe1b423479bc64ff49..7770a7586c7892db6ef3b780845466034e516fb5 100644 (file)
@@ -3,14 +3,16 @@ import { atom } from 'jotai';
 const STORAGE_KEY = 'settings';
 export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
 export type MessageLayout = 0 | 1 | 2;
+
 export interface Settings {
   themeIndex: number;
   useSystemTheme: boolean;
   isMarkdown: boolean;
   editorToolbar: boolean;
-  isPeopleDrawer: boolean;
   useSystemEmoji: boolean;
 
+  isPeopleDrawer: boolean;
+  memberSortFilterIndex: number;
   enterForNewline: boolean;
   messageLayout: MessageLayout;
   messageSpacing: MessageSpacing;
@@ -28,9 +30,10 @@ const defaultSettings: Settings = {
   useSystemTheme: true,
   isMarkdown: true,
   editorToolbar: false,
-  isPeopleDrawer: true,
   useSystemEmoji: false,
 
+  isPeopleDrawer: true,
+  memberSortFilterIndex: 0,
   enterForNewline: false,
   messageLayout: 0,
   messageSpacing: '400',
index 1dabdc0748515910a6f7edace13c6243d02bd742..1c015a1ffbb5ec77ecc8eccc6de56da97af11956 100644 (file)
@@ -13,6 +13,7 @@ import {
   NotificationCountType,
   RelationType,
   Room,
+  RoomMember,
 } from 'matrix-js-sdk';
 import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
 import { AccountDataEvent } from '../../types/matrix/accountData';
@@ -293,6 +294,15 @@ export const getMemberDisplayName = (room: Room, userId: string): string | undef
   return name;
 };
 
+export const getMemberSearchStr = (
+  member: RoomMember,
+  query: string,
+  mxIdToName: (mxId: string) => string
+): string[] => [
+  member.rawDisplayName === member.userId ? mxIdToName(member.userId) : member.rawDisplayName,
+  query.startsWith('@') || query.indexOf(':') > -1 ? member.userId : mxIdToName(member.userId),
+];
+
 export const getMemberAvatarMxc = (room: Room, userId: string): string | undefined => {
   const member = room.getMember(userId);
   return member?.getMxcAvatarUrl();