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';
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;
}
type UserMentionAutocompleteProps = {
- roomId: string;
+ room: Room;
editor: Editor;
query: AutocompleteQuery<string>;
requestClose: () => void;
},
};
-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);
});
});
+ 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' && (
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}
before={
<Avatar size="200">
{avatarUrl ? (
- <AvatarImage src={avatarUrl} alt={roomMember.userId} />
+ <AvatarImage src={avatarUrl} alt={getName(roomMember)} />
) : (
<AvatarFallback
style={{
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>
);
};
export type SearchItemStrGetter<TSearchItem extends object | string | number> = (
- searchItem: TSearchItem
+ searchItem: TSearchItem,
+ query: string
) => string | string[];
export type UseAsyncSearchResult<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)
} 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,
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;
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(
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);
}}
>
<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);
}}
>
<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>
}}
>
<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);
}}
>
radii="300"
after={<Icon src={Icons.Sort} size="50" />}
>
- <Text size="T200">{filter.sortFilter.name}</Text>
+ <Text size="T200">{sortFilter.name}</Text>
</Chip>
)}
</PopOut>
{!fetchingMembers && !result && processMembers.length === 0 && (
<Text style={{ padding: config.space.S300 }} align="Center">
- {`No "${filter.membershipFilter.name}" Members`}
+ {`No "${membershipFilter.name}" Members`}
</Text>
)}
{screenSize === ScreenSize.Desktop && isDrawer && (
<>
<Line variant="Background" direction="Vertical" size="300" />
- <MembersDrawer room={room} />
+ <MembersDrawer key={room.roomId} room={room} />
</>
)}
</div>
)}
{autocompleteQuery?.prefix === AutocompletePrefix.UserMention && (
<UserMentionAutocomplete
- roomId={roomId}
+ room={room}
editor={editor}
query={autocompleteQuery}
requestClose={handleCloseAutocomplete}
)}
{autocompleteQuery?.prefix === AutocompletePrefix.UserMention && (
<UserMentionAutocomplete
- roomId={roomId}
+ room={room}
editor={editor}
query={autocompleteQuery}
requestClose={handleCloseAutocomplete}
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;
useSystemTheme: true,
isMarkdown: true,
editorToolbar: false,
- isPeopleDrawer: true,
useSystemEmoji: false,
+ isPeopleDrawer: true,
+ memberSortFilterIndex: 0,
enterForNewline: false,
messageLayout: 0,
messageSpacing: '400',
NotificationCountType,
RelationType,
Room,
+ RoomMember,
} from 'matrix-js-sdk';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import { AccountDataEvent } from '../../types/matrix/accountData';
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();