xinny/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
Ajay Bura 0b06bed1db
Refactor state & Custom editor (#1190)
* Fix eslint

* Enable ts strict mode

* install folds, jotai & immer

* Enable immer map/set

* change cross-signing alert anim to 30 iteration

* Add function to access matrix client

* Add new types

* Add disposable util

* Add room utils

* Add mDirect list atom

* Add invite list atom

* add room list atom

* add utils for jotai atoms

* Add room id to parents atom

* Add mute list atom

* Add room to unread atom

* Use hook to bind atoms with sdk

* Add settings atom

* Add settings hook

* Extract set settings hook

* Add Sidebar components

* WIP

* Add bind atoms hook

* Fix init muted room list atom

* add navigation atoms

* Add custom editor

* Fix hotkeys

* Update folds

* Add editor output function

* Add matrix client context

* Add tooltip to editor toolbar items

* WIP - Add editor to room input

* Refocus editor on toolbar item click

* Add Mentions - WIP

* update folds

* update mention focus outline

* rename emoji element type

* Add auto complete menu

* add autocomplete query functions

* add index file for editor

* fix bug in getPrevWord function

* Show room mention autocomplete

* Add async search function

* add use async search hook

* use async search in room mention autocomplete

* remove folds prefer font for now

* allow number array in async search

* reset search with empty query

* Autocomplete unknown room mention

* Autocomplete first room mention on tab

* fix roomAliasFromQueryText

* change mention color to primary

* add isAlive hook

* add getMxIdLocalPart to mx utils

* fix getRoomAvatarUrl size

* fix types

* add room members hook

* fix bug in room mention

* add user mention autocomplete

* Fix async search giving prev result after no match

* update folds

* add twemoji font

* add use state provider hook

* add prevent scroll with arrow key util

* add ts to custom-emoji and emoji files

* add types

* add hook for emoji group labels

* add hook for emoji group icons

* add emoji board with basic emoji

* add emojiboard in room input

* select multiple emoji with shift press

* display custom emoji in emojiboard

* Add emoji preview

* focus element on hover

* update folds

* position emojiboard properly

* convert recent-emoji.js to ts

* add use recent emoji hook

* add io.element.recent_emoji to account data evt

* Render recent emoji in emoji board

* show custom emoji from parent spaces

* show room emoji

* improve emoji sidebar

* update folds

* fix pack avatar and name fallback in emoji board

* add stickers to emoji board

* fix bug in emoji preview

* Add sticker icon in room input

* add debounce hook

* add search in emoji board

* Optimize emoji board

* fix emoji board sidebar divider

* sync emojiboard sidebar with scroll & update ui

* Add use throttle hook

* support custom emoji in editor

* remove duplicate emoji selection function

* fix emoji and mention spacing

* add emoticon autocomplete in editor

* fix string

* makes emoji size relative to font size in editor

* add option to render link element

* add spoiler in editor

* fix sticker in emoji board search using wrong type

* render custom placeholder

* update hotkey for block quote and block code

* add terminate search function in async search

* add getImageInfo to matrix utils

* send stickers

* add resize observer hook

* move emoji board component hooks in hooks dir

* prevent editor expand hides room timeline

* send typing notifications

* improve emoji style and performance

* fix imports

* add on paste param to editor

* add selectFile utils

* add file picker hook

* add file paste handler hook

* add file drop handler

* update folds

* Add file upload card

* add bytes to size util

* add blurHash util

* add await to js lib

* add browser-encrypt-attachment types

* add list atom

* convert mimetype file to ts

* add matrix types

* add matrix file util

* add file related dom utils

* add common utils

* add upload atom

* add room input draft atom

* add upload card renderer component

* add upload board component

* add support for file upload in editor

* send files with message / enter

* fix circular deps

* store editor toolbar state in local store

* move msg content util to separate file

* store msg draft on room switch

* fix following member not updating on msg sent

* add theme for folds component

* fix system default theme

* Add reply support in editor

* prevent initMatrix to init multiple time

* add state event hooks

* add async callback hook

* Show tombstone info for tombstone room

* fix room tombstone component border

* add power level hook

* Add room input placeholder component

* Show input placeholder for muted member
2023-06-12 16:45:23 +05:30

192 lines
5.9 KiB
TypeScript

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 { 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 '../common';
import { useKeyDown } from '../../../hooks/useKeyDown';
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
type MentionAutoCompleteHandler = (userId: string, name: string) => void;
const userIdFromQueryText = (mx: MatrixClient, text: string) =>
validMxId(`@${text}`)
? `@${text}`
: `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
function UnknownMentionItem({
query,
userId,
name,
handleAutocomplete,
}: {
query: AutocompleteQuery<string>;
userId: string;
name: string;
handleAutocomplete: MentionAutoCompleteHandler;
}) {
return (
<MenuItem
as="button"
radii="300"
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
onTabPress(evt, () => handleAutocomplete(userId, name))
}
onClick={() => handleAutocomplete(userId, name)}
before={
<Avatar size="200">
<AvatarFallback
style={{
backgroundColor: color.Secondary.Container,
color: color.Secondary.OnContainer,
}}
>
<Text size="H6">{query.text[0]}</Text>
</AvatarFallback>
</Avatar>
}
>
<Text style={{ flexGrow: 1 }} size="B400">
{name}
</Text>
</MenuItem>
);
}
type UserMentionAutocompleteProps = {
roomId: string;
editor: Editor;
query: AutocompleteQuery<string>;
requestClose: () => void;
};
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 20,
matchOptions: {
contain: true,
},
};
const getRoomMemberStr: SearchItemStrGetter<RoomMember> = (roomMember) => [
roomMember.name,
getMxIdLocalPart(roomMember.userId) ?? roomMember.userId,
roomMember.userId,
];
export function UserMentionAutocomplete({
roomId,
editor,
query,
requestClose,
}: UserMentionAutocompleteProps) {
const mx = useMatrixClient();
const room = mx.getRoom(roomId);
const roomAliasOrId = room?.getCanonicalAlias() || roomId;
const members = useRoomMembers(mx, roomId);
const [result, search] = useAsyncSearch(members, getRoomMemberStr, SEARCH_OPTIONS);
const autoCompleteMembers = result ? result.items : members.slice(0, 20);
useEffect(() => {
search(query.text);
}, [query.text, search]);
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);
});
});
return (
<AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}>
{query.text === 'room' && (
<UnknownMentionItem
query={query}
userId={roomAliasOrId}
name="@room"
handleAutocomplete={handleAutocomplete}
/>
)}
{autoCompleteMembers.length === 0 ? (
<UnknownMentionItem
query={query}
userId={userIdFromQueryText(mx, query.text)}
name={userIdFromQueryText(mx, query.text)}
handleAutocomplete={handleAutocomplete}
/>
) : (
autoCompleteMembers.map((roomMember) => {
const avatarUrl = roomMember.getAvatarUrl(mx.baseUrl, 32, 32, 'crop', undefined, false);
return (
<MenuItem
key={roomMember.userId}
as="button"
radii="300"
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
onTabPress(evt, () => handleAutocomplete(roomMember.userId, roomMember.name))
}
onClick={() => handleAutocomplete(roomMember.userId, roomMember.name)}
after={
<Text size="T200" priority="300" truncate>
{roomMember.userId}
</Text>
}
before={
<Avatar size="200">
{avatarUrl ? (
<AvatarImage src={avatarUrl} alt={roomMember.userId} />
) : (
<AvatarFallback
style={{
backgroundColor: color.Secondary.Container,
color: color.Secondary.OnContainer,
}}
>
<Text size="H6">{roomMember.name[0] || roomMember.userId[1]}</Text>
</AvatarFallback>
)}
</Avatar>
}
>
<Text style={{ flexGrow: 1 }} size="B400" truncate>
{roomMember.name}
</Text>
</MenuItem>
);
})
)}
</AutocompleteMenu>
);
}