diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx index a3243ba61..91fffbab0 100644 --- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx @@ -12,6 +12,7 @@ import { AutocompleteMenu } from './AutocompleteMenu'; import { getMxIdServer, isRoomAlias } from '../../../utils/matrix'; import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch'; import { onTabPress } from '../../../utils/keyboard'; +import { useAutocompleteEnter } from '../../../hooks/useAutocompleteEnter'; import { useKeyDown } from '../../../hooks/useKeyDown'; import { mDirectAtom } from '../../../state/mDirectList'; import { allRoomsAtom } from '../../../state/room-list/roomList'; @@ -126,19 +127,20 @@ export function RoomMentionAutocomplete({ requestClose(); }; - useKeyDown(window, (evt: KeyboardEvent) => { - onTabPress(evt, () => { - if (autoCompleteRoomIds.length === 0) { - const alias = roomAliasFromQueryText(mx, query.text); - handleAutocomplete(alias, alias); - return; - } - const rId = autoCompleteRoomIds[0]; - const r = mx.getRoom(rId); - const name = r?.name ?? rId; - handleAutocomplete(r?.getCanonicalAlias() ?? rId, name); - }); - }); + const pickTop = () => { + if (autoCompleteRoomIds.length === 0) { + const alias = roomAliasFromQueryText(mx, query.text); + handleAutocomplete(alias, alias); + return; + } + const rId = autoCompleteRoomIds[0]; + const r = mx.getRoom(rId); + const name = r?.name ?? rId; + handleAutocomplete(r?.getCanonicalAlias() ?? rId, name); + }; + + useKeyDown(window, (evt: KeyboardEvent) => onTabPress(evt, pickTop)); + useAutocompleteEnter(true, pickTop); return ( Rooms} requestClose={requestClose}> diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx index 663e49fb6..9ab348290 100644 --- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx @@ -13,6 +13,7 @@ import { useAsyncSearch, } from '../../../hooks/useAsyncSearch'; import { onTabPress } from '../../../utils/keyboard'; +import { useAutocompleteEnter } from '../../../hooks/useAutocompleteEnter'; import { createMentionElement, moveCursor, replaceWithElement } from '../utils'; import { useKeyDown } from '../../../hooks/useKeyDown'; import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix'; @@ -124,21 +125,24 @@ export function UserMentionAutocomplete({ 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 pickTop = () => { + 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, getMemberName(room, roomMember.userId)); + }; + + useKeyDown(window, (evt: KeyboardEvent) => onTabPress(evt, pickTop)); + // The list always shows a suggestion (a member, or the typed id), so Enter + // always picks rather than sending "hey @bo". + useAutocompleteEnter(true, pickTop); const getName = (member: RoomMember) => getMemberName(room, member.userId); diff --git a/src/app/features/room/CommandAutocomplete.tsx b/src/app/features/room/CommandAutocomplete.tsx index 891c00f4a..a24f20c5e 100644 --- a/src/app/features/room/CommandAutocomplete.tsx +++ b/src/app/features/room/CommandAutocomplete.tsx @@ -14,6 +14,7 @@ import { UseAsyncSearchOptions, useAsyncSearch } from '../../hooks/useAsyncSearc import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useKeyDown } from '../../hooks/useKeyDown'; import { onTabPress } from '../../utils/keyboard'; +import { useAutocompleteEnter } from '../../hooks/useAutocompleteEnter'; type CommandAutoCompleteHandler = (commandName: string) => void; @@ -60,15 +61,14 @@ export function CommandAutocomplete({ requestClose(); }; - useKeyDown(window, (evt: KeyboardEvent) => { - onTabPress(evt, () => { - if (autoCompleteNames.length === 0) { - return; - } - const cmdName = autoCompleteNames[0]; - handleAutocomplete(cmdName); - }); - }); + const pickTop = () => { + if (autoCompleteNames.length === 0) return; + handleAutocomplete(autoCompleteNames[0]); + }; + + useKeyDown(window, (evt: KeyboardEvent) => onTabPress(evt, pickTop)); + // Only while suggestions show: with no match Enter must still send. + useAutocompleteEnter(autoCompleteNames.length > 0, pickTop); return autoCompleteNames.length === 0 ? null : ( void): void { + const selectRef = useRef(onSelect); + selectRef.current = onSelect; + + useEffect(() => { + if (!active) return undefined; + const onKeyDown = (evt: KeyboardEvent) => { + if (evt.isComposing || !isKeyHotkey('enter', evt)) return; + evt.preventDefault(); + evt.stopPropagation(); + selectRef.current(); + }; + window.addEventListener('keydown', onKeyDown, true); + return () => window.removeEventListener('keydown', onKeyDown, true); + }, [active]); +}