import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react'; import { Editor } from 'slate'; import { Avatar, Icon, Icons, MenuItem, Text } from 'folds'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; import { AutocompleteQuery } from './autocompleteQuery'; import { AutocompleteMenu } from './AutocompleteMenu'; import { useRoomMembers } from '../../../hooks/useRoomMembers'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { SearchItemStrGetter, UseAsyncSearchOptions, useAsyncSearch, } from '../../../hooks/useAsyncSearch'; import { onTabPress } from '../../../utils/keyboard'; import { createMentionElement, moveCursor, replaceWithElement } from '../utils'; import { useKeyDown } from '../../../hooks/useKeyDown'; import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix'; import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room'; import { UserAvatar } from '../../user-avatar'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { Membership } from '../../../../types/matrix/room'; type MentionAutoCompleteHandler = (userId: string, name: string) => void; const userIdFromQueryText = (mx: MatrixClient, text: string) => isUserId(`@${text}`) ? `@${text}` : `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`; function UnknownMentionItem({ userId, name, handleAutocomplete, }: { userId: string; name: string; handleAutocomplete: MentionAutoCompleteHandler; }) { return ( ) => onTabPress(evt, () => handleAutocomplete(userId, name)) } onClick={() => handleAutocomplete(userId, name)} before={ } /> } > {name} ); } type UserMentionAutocompleteProps = { room: Room; editor: Editor; query: AutocompleteQuery; requestClose: () => void; }; const withAllowedMembership = (member: RoomMember): boolean => member.membership === Membership.Join || member.membership === Membership.Invite || member.membership === Membership.Knock; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, matchOptions: { contain: true, }, }; const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId; const getRoomMemberStr: SearchItemStrGetter = (m, query) => getMemberSearchStr(m, query, mxIdToName); export function UserMentionAutocomplete({ room, editor, query, requestClose, }: UserMentionAutocompleteProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); 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 autoCompleteMembers = (result ? result.items.slice(0, 20) : members.slice(0, 20)).filter( withAllowedMembership ); useEffect(() => { if (query.text) search(query.text); else resetSearch(); }, [query.text, search, resetSearch]); const handleAutocomplete: MentionAutoCompleteHandler = (uId, name) => { const mentionEl = createMentionElement( uId, name.startsWith('@') ? name : `@${name}`, mx.getUserId() === uId || roomAliasOrId === uId ); replaceWithElement(editor, query.range, mentionEl); moveCursor(editor, true); requestClose(); }; useKeyDown(window, (evt: KeyboardEvent) => { onTabPress(evt, () => { if (query.text === 'room') { handleAutocomplete(roomAliasOrId, '@room'); return; } if (autoCompleteMembers.length === 0) { const userId = userIdFromQueryText(mx, query.text); handleAutocomplete(userId, userId); return; } const roomMember = autoCompleteMembers[0]; handleAutocomplete(roomMember.userId, roomMember.name); }); }); const getName = (member: RoomMember) => getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId; return ( Mentions} requestClose={requestClose}> {query.text === 'room' && ( )} {autoCompleteMembers.length === 0 ? ( ) : ( autoCompleteMembers.map((roomMember) => { const avatarMxcUrl = roomMember.getMxcAvatarUrl(); const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(avatarMxcUrl, 32, 32, 'crop', undefined, false, useAuthentication) : undefined; return ( ) => onTabPress(evt, () => handleAutocomplete(roomMember.userId, getName(roomMember))) } onClick={() => handleAutocomplete(roomMember.userId, getName(roomMember))} after={ {roomMember.userId} } before={ } /> } > {getName(roomMember)} ); }) )} ); }