Member drawer filter (#1457)

* save member drawer sort filter in local storage

* render member drawer with key

* improve member search
This commit is contained in:
Ajay Bura 2023-10-19 17:43:16 +11:00 committed by GitHub
parent b4e1ced3ed
commit 50429a3513
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 44 deletions

View file

@ -1,7 +1,7 @@
import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react'; import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
import { Editor } from 'slate'; import { Editor } from 'slate';
import { Avatar, AvatarFallback, AvatarImage, MenuItem, Text, color } from 'folds'; 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 { AutocompleteQuery } from './autocompleteQuery';
import { AutocompleteMenu } from './AutocompleteMenu'; import { AutocompleteMenu } from './AutocompleteMenu';
@ -16,6 +16,7 @@ import { onTabPress } from '../../../utils/keyboard';
import { createMentionElement, moveCursor, replaceWithElement } from '../utils'; import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
import { useKeyDown } from '../../../hooks/useKeyDown'; import { useKeyDown } from '../../../hooks/useKeyDown';
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix'; import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
type MentionAutoCompleteHandler = (userId: string, name: string) => void; type MentionAutoCompleteHandler = (userId: string, name: string) => void;
@ -64,7 +65,7 @@ function UnknownMentionItem({
} }
type UserMentionAutocompleteProps = { type UserMentionAutocompleteProps = {
roomId: string; room: Room;
editor: Editor; editor: Editor;
query: AutocompleteQuery<string>; query: AutocompleteQuery<string>;
requestClose: () => void; requestClose: () => void;
@ -77,21 +78,19 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = {
}, },
}; };
const getRoomMemberStr: SearchItemStrGetter<RoomMember> = (roomMember) => [ const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId;
roomMember.name, const getRoomMemberStr: SearchItemStrGetter<RoomMember> = (m, query) =>
getMxIdLocalPart(roomMember.userId) ?? roomMember.userId, getMemberSearchStr(m, query, mxIdToName);
roomMember.userId,
];
export function UserMentionAutocomplete({ export function UserMentionAutocomplete({
roomId, room,
editor, editor,
query, query,
requestClose, requestClose,
}: UserMentionAutocompleteProps) { }: UserMentionAutocompleteProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = mx.getRoom(roomId); const roomId: string = room.roomId!;
const roomAliasOrId = room?.getCanonicalAlias() || roomId; const roomAliasOrId = room.getCanonicalAlias() || roomId;
const members = useRoomMembers(mx, roomId); const members = useRoomMembers(mx, roomId);
const [result, search, resetSearch] = useAsyncSearch(members, getRoomMemberStr, SEARCH_OPTIONS); 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 ( return (
<AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}> <AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}>
{query.text === 'room' && ( {query.text === 'room' && (
@ -155,9 +157,9 @@ export function UserMentionAutocomplete({
as="button" as="button"
radii="300" radii="300"
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) => 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={ after={
<Text size="T200" priority="300" truncate> <Text size="T200" priority="300" truncate>
{roomMember.userId} {roomMember.userId}
@ -166,7 +168,7 @@ export function UserMentionAutocomplete({
before={ before={
<Avatar size="200"> <Avatar size="200">
{avatarUrl ? ( {avatarUrl ? (
<AvatarImage src={avatarUrl} alt={roomMember.userId} /> <AvatarImage src={avatarUrl} alt={getName(roomMember)} />
) : ( ) : (
<AvatarFallback <AvatarFallback
style={{ style={{
@ -174,14 +176,14 @@ export function UserMentionAutocomplete({
color: color.Secondary.OnContainer, color: color.Secondary.OnContainer,
}} }}
> >
<Text size="H6">{roomMember.name[0] || roomMember.userId[1]}</Text> <Text size="H6">{getName(roomMember)[0]}</Text>
</AvatarFallback> </AvatarFallback>
)} )}
</Avatar> </Avatar>
} }
> >
<Text style={{ flexGrow: 1 }} size="B400" truncate> <Text style={{ flexGrow: 1 }} size="B400" truncate>
{roomMember.name} {getName(roomMember)}
</Text> </Text>
</MenuItem> </MenuItem>
); );

View file

@ -17,7 +17,8 @@ export type UseAsyncSearchOptions = AsyncSearchOption & {
}; };
export type SearchItemStrGetter<TSearchItem extends object | string | number> = ( export type SearchItemStrGetter<TSearchItem extends object | string | number> = (
searchItem: TSearchItem searchItem: TSearchItem,
query: string
) => string | string[]; ) => string | string[];
export type UseAsyncSearchResult<TSearchItem extends object | string | number> = { export type UseAsyncSearchResult<TSearchItem extends object | string | number> = {
@ -38,7 +39,7 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
setResult(undefined); setResult(undefined);
const handleMatch: MatchHandler<TSearchItem> = (item, query) => { const handleMatch: MatchHandler<TSearchItem> = (item, query) => {
const itemStr = getItemStr(item); const itemStr = getItemStr(item, query);
if (Array.isArray(itemStr)) if (Array.isArray(itemStr))
return !!itemStr.find((i) => return !!itemStr.find((i) =>
matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions) matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)

View file

@ -46,14 +46,20 @@ import {
} from '../../hooks/useIntersectionObserver'; } from '../../hooks/useIntersectionObserver';
import { Membership } from '../../../types/matrix/room'; import { Membership } from '../../../types/matrix/room';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
import { UseAsyncSearchOptions, useAsyncSearch } from '../../hooks/useAsyncSearch'; import {
SearchItemStrGetter,
UseAsyncSearchOptions,
useAsyncSearch,
} from '../../hooks/useAsyncSearch';
import { useDebounce } from '../../hooks/useDebounce'; import { useDebounce } from '../../hooks/useDebounce';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { usePowerLevelTags, PowerLevelTag } from '../../hooks/usePowerLevelTags'; import { usePowerLevelTags, PowerLevelTag } from '../../hooks/usePowerLevelTags';
import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../../state/typingMembers'; import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../../state/typingMembers';
import { TypingIndicator } from '../../components/typing-indicator'; import { TypingIndicator } from '../../components/typing-indicator';
import { getMemberDisplayName } from '../../utils/room'; import { getMemberDisplayName, getMemberSearchStr } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart } from '../../utils/matrix';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
export const MembershipFilters = { export const MembershipFilters = {
filterJoined: (m: RoomMember) => m.membership === Membership.Join, filterJoined: (m: RoomMember) => m.membership === Membership.Join,
@ -159,7 +165,10 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = {
contain: true, 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 = { type MembersDrawerProps = {
room: Room; room: Room;
@ -175,10 +184,12 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
const membershipFilterMenu = useMembershipFilterMenu(); const membershipFilterMenu = useMembershipFilterMenu();
const sortFilterMenu = useSortFilterMenu(); const sortFilterMenu = useSortFilterMenu();
const [filter, setFilter] = useState<MembersFilterOptions>({ const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
membershipFilter: membershipFilterMenu[0], const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
sortFilter: sortFilterMenu[0],
}); const membershipFilter = membershipFilterMenu[membershipFilterIndex] ?? membershipFilterMenu[0];
const sortFilter = sortFilterMenu[sortFilterIndex] ?? sortFilterMenu[0];
const [onTop, setOnTop] = useState(true); const [onTop, setOnTop] = useState(true);
const typingMembers = useAtomValue( const typingMembers = useAtomValue(
@ -188,15 +199,15 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
const filteredMembers = useMemo( const filteredMembers = useMemo(
() => () =>
members members
.filter(filter.membershipFilter.filterFn) .filter(membershipFilter.filterFn)
.sort(filter.sortFilter.filterFn) .sort(sortFilter.filterFn)
.sort((a, b) => b.powerLevel - a.powerLevel), .sort((a, b) => b.powerLevel - a.powerLevel),
[members, filter] [members, membershipFilter, sortFilter]
); );
const [result, search, resetSearch] = useAsyncSearch( const [result, search, resetSearch] = useAsyncSearch(
filteredMembers, filteredMembers,
getMemberItemStr, getRoomMemberStr,
SEARCH_OPTIONS SEARCH_OPTIONS
); );
if (!result && searchInputRef.current?.value) search(searchInputRef.current.value); if (!result && searchInputRef.current?.value) search(searchInputRef.current.value);
@ -310,18 +321,18 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S100 }}>
{membershipFilterMenu.map((menuItem) => ( {membershipFilterMenu.map((menuItem, index) => (
<MenuItem <MenuItem
key={menuItem.name} key={menuItem.name}
variant={ variant={
menuItem.name === filter.membershipFilter.name menuItem.name === membershipFilter.name
? menuItem.color ? menuItem.color
: 'Surface' : 'Surface'
} }
aria-pressed={menuItem.name === filter.membershipFilter.name} aria-pressed={menuItem.name === membershipFilter.name}
radii="300" radii="300"
onClick={() => { onClick={() => {
setFilter((f) => ({ ...f, membershipFilter: menuItem })); setMembershipFilterIndex(index);
setOpen(false); setOpen(false);
}} }}
> >
@ -336,12 +347,12 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
<Chip <Chip
ref={anchorRef} ref={anchorRef}
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
variant={filter.membershipFilter.color} variant={membershipFilter.color}
size="400" size="400"
radii="300" radii="300"
before={<Icon src={Icons.Filter} size="50" />} before={<Icon src={Icons.Filter} size="50" />}
> >
<Text size="T200">{filter.membershipFilter.name}</Text> <Text size="T200">{membershipFilter.name}</Text>
</Chip> </Chip>
)} )}
</PopOut> </PopOut>
@ -365,14 +376,14 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
}} }}
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S100 }}>
{sortFilterMenu.map((menuItem) => ( {sortFilterMenu.map((menuItem, index) => (
<MenuItem <MenuItem
key={menuItem.name} key={menuItem.name}
variant="Surface" variant="Surface"
aria-pressed={menuItem.name === filter.sortFilter.name} aria-pressed={menuItem.name === sortFilter.name}
radii="300" radii="300"
onClick={() => { onClick={() => {
setFilter((f) => ({ ...f, sortFilter: menuItem })); setSortFilterIndex(index);
setOpen(false); setOpen(false);
}} }}
> >
@ -392,7 +403,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
radii="300" radii="300"
after={<Icon src={Icons.Sort} size="50" />} after={<Icon src={Icons.Sort} size="50" />}
> >
<Text size="T200">{filter.sortFilter.name}</Text> <Text size="T200">{sortFilter.name}</Text>
</Chip> </Chip>
)} )}
</PopOut> </PopOut>
@ -452,7 +463,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
{!fetchingMembers && !result && processMembers.length === 0 && ( {!fetchingMembers && !result && processMembers.length === 0 && (
<Text style={{ padding: config.space.S300 }} align="Center"> <Text style={{ padding: config.space.S300 }} align="Center">
{`No "${filter.membershipFilter.name}" Members`} {`No "${membershipFilter.name}" Members`}
</Text> </Text>
)} )}

View file

@ -37,7 +37,7 @@ export function RoomBaseView({ room, eventId }: RoomBaseViewProps) {
{screenSize === ScreenSize.Desktop && isDrawer && ( {screenSize === ScreenSize.Desktop && isDrawer && (
<> <>
<Line variant="Background" direction="Vertical" size="300" /> <Line variant="Background" direction="Vertical" size="300" />
<MembersDrawer room={room} /> <MembersDrawer key={room.roomId} room={room} />
</> </>
)} )}
</div> </div>

View file

@ -443,7 +443,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
)} )}
{autocompleteQuery?.prefix === AutocompletePrefix.UserMention && ( {autocompleteQuery?.prefix === AutocompletePrefix.UserMention && (
<UserMentionAutocomplete <UserMentionAutocomplete
roomId={roomId} room={room}
editor={editor} editor={editor}
query={autocompleteQuery} query={autocompleteQuery}
requestClose={handleCloseAutocomplete} requestClose={handleCloseAutocomplete}

View file

@ -193,7 +193,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
)} )}
{autocompleteQuery?.prefix === AutocompletePrefix.UserMention && ( {autocompleteQuery?.prefix === AutocompletePrefix.UserMention && (
<UserMentionAutocomplete <UserMentionAutocomplete
roomId={roomId} room={room}
editor={editor} editor={editor}
query={autocompleteQuery} query={autocompleteQuery}
requestClose={handleCloseAutocomplete} requestClose={handleCloseAutocomplete}

View file

@ -3,14 +3,16 @@ import { atom } from 'jotai';
const STORAGE_KEY = 'settings'; const STORAGE_KEY = 'settings';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500'; export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export type MessageLayout = 0 | 1 | 2; export type MessageLayout = 0 | 1 | 2;
export interface Settings { export interface Settings {
themeIndex: number; themeIndex: number;
useSystemTheme: boolean; useSystemTheme: boolean;
isMarkdown: boolean; isMarkdown: boolean;
editorToolbar: boolean; editorToolbar: boolean;
isPeopleDrawer: boolean;
useSystemEmoji: boolean; useSystemEmoji: boolean;
isPeopleDrawer: boolean;
memberSortFilterIndex: number;
enterForNewline: boolean; enterForNewline: boolean;
messageLayout: MessageLayout; messageLayout: MessageLayout;
messageSpacing: MessageSpacing; messageSpacing: MessageSpacing;
@ -28,9 +30,10 @@ const defaultSettings: Settings = {
useSystemTheme: true, useSystemTheme: true,
isMarkdown: true, isMarkdown: true,
editorToolbar: false, editorToolbar: false,
isPeopleDrawer: true,
useSystemEmoji: false, useSystemEmoji: false,
isPeopleDrawer: true,
memberSortFilterIndex: 0,
enterForNewline: false, enterForNewline: false,
messageLayout: 0, messageLayout: 0,
messageSpacing: '400', messageSpacing: '400',

View file

@ -13,6 +13,7 @@ import {
NotificationCountType, NotificationCountType,
RelationType, RelationType,
Room, Room,
RoomMember,
} from 'matrix-js-sdk'; } from 'matrix-js-sdk';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import { AccountDataEvent } from '../../types/matrix/accountData'; import { AccountDataEvent } from '../../types/matrix/accountData';
@ -293,6 +294,15 @@ export const getMemberDisplayName = (room: Room, userId: string): string | undef
return name; 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 => { export const getMemberAvatarMxc = (room: Room, userId: string): string | undefined => {
const member = room.getMember(userId); const member = room.getMember(userId);
return member?.getMxcAvatarUrl(); return member?.getMxcAvatarUrl();