From 50429a3513135dbb5dcbd5e376539dfce53cf330 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:43:16 +1100 Subject: [PATCH] Member drawer filter (#1457) * save member drawer sort filter in local storage * render member drawer with key * improve member search --- .../autocomplete/UserMentionAutocomplete.tsx | 32 ++++++----- src/app/hooks/useAsyncSearch.ts | 5 +- src/app/organisms/room/MembersDrawer.tsx | 55 +++++++++++-------- src/app/organisms/room/Room.tsx | 2 +- src/app/organisms/room/RoomInput.tsx | 2 +- .../organisms/room/message/MessageEditor.tsx | 2 +- src/app/state/settings.ts | 7 ++- src/app/utils/room.ts | 10 ++++ 8 files changed, 71 insertions(+), 44 deletions(-) diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx index a99274a..8c3d91b 100644 --- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx @@ -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; requestClose: () => void; @@ -77,21 +78,19 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = { }, }; -const getRoomMemberStr: SearchItemStrGetter = (roomMember) => [ - roomMember.name, - getMxIdLocalPart(roomMember.userId) ?? roomMember.userId, - roomMember.userId, -]; +const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId; +const getRoomMemberStr: SearchItemStrGetter = (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 ( Mentions} requestClose={requestClose}> {query.text === 'room' && ( @@ -155,9 +157,9 @@ export function UserMentionAutocomplete({ as="button" radii="300" onKeyDown={(evt: ReactKeyboardEvent) => - 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={ {roomMember.userId} @@ -166,7 +168,7 @@ export function UserMentionAutocomplete({ before={ {avatarUrl ? ( - + ) : ( - {roomMember.name[0] || roomMember.userId[1]} + {getName(roomMember)[0]} )} } > - {roomMember.name} + {getName(roomMember)} ); diff --git a/src/app/hooks/useAsyncSearch.ts b/src/app/hooks/useAsyncSearch.ts index d0e73e7..719e0d5 100644 --- a/src/app/hooks/useAsyncSearch.ts +++ b/src/app/hooks/useAsyncSearch.ts @@ -17,7 +17,8 @@ export type UseAsyncSearchOptions = AsyncSearchOption & { }; export type SearchItemStrGetter = ( - searchItem: TSearchItem + searchItem: TSearchItem, + query: string ) => string | string[]; export type UseAsyncSearchResult = { @@ -38,7 +39,7 @@ export const useAsyncSearch = ( setResult(undefined); const handleMatch: MatchHandler = (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) diff --git a/src/app/organisms/room/MembersDrawer.tsx b/src/app/organisms/room/MembersDrawer.tsx index d0bb55d..365dc62 100644 --- a/src/app/organisms/room/MembersDrawer.tsx +++ b/src/app/organisms/room/MembersDrawer.tsx @@ -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 = (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({ - 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) { }} > - {membershipFilterMenu.map((menuItem) => ( + {membershipFilterMenu.map((menuItem, index) => ( { - setFilter((f) => ({ ...f, membershipFilter: menuItem })); + setMembershipFilterIndex(index); setOpen(false); }} > @@ -336,12 +347,12 @@ export function MembersDrawer({ room }: MembersDrawerProps) { setOpen(!open)} - variant={filter.membershipFilter.color} + variant={membershipFilter.color} size="400" radii="300" before={} > - {filter.membershipFilter.name} + {membershipFilter.name} )} @@ -365,14 +376,14 @@ export function MembersDrawer({ room }: MembersDrawerProps) { }} > - {sortFilterMenu.map((menuItem) => ( + {sortFilterMenu.map((menuItem, index) => ( { - setFilter((f) => ({ ...f, sortFilter: menuItem })); + setSortFilterIndex(index); setOpen(false); }} > @@ -392,7 +403,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) { radii="300" after={} > - {filter.sortFilter.name} + {sortFilter.name} )} @@ -452,7 +463,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) { {!fetchingMembers && !result && processMembers.length === 0 && ( - {`No "${filter.membershipFilter.name}" Members`} + {`No "${membershipFilter.name}" Members`} )} diff --git a/src/app/organisms/room/Room.tsx b/src/app/organisms/room/Room.tsx index 094daad..6158547 100644 --- a/src/app/organisms/room/Room.tsx +++ b/src/app/organisms/room/Room.tsx @@ -37,7 +37,7 @@ export function RoomBaseView({ room, eventId }: RoomBaseViewProps) { {screenSize === ScreenSize.Desktop && isDrawer && ( <> - + )} diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index 81c29b0..ae12afb 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -443,7 +443,7 @@ export const RoomInput = forwardRef( )} {autocompleteQuery?.prefix === AutocompletePrefix.UserMention && ( ( )} {autocompleteQuery?.prefix === AutocompletePrefix.UserMention && ( 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();