diff --git a/components.json b/components.json index e44b3e4254..ab9cccbdab 100644 --- a/components.json +++ b/components.json @@ -2,7 +2,6 @@ "src/components/views/auth/AuthFooter.tsx": "src/components/views/auth/VectorAuthFooter.tsx", "src/components/views/auth/AuthHeaderLogo.tsx": "src/components/views/auth/VectorAuthHeaderLogo.tsx", "src/components/views/auth/AuthPage.tsx": "src/components/views/auth/VectorAuthPage.tsx", - "src/components/views/rooms/Autocomplete.tsx": "src/components/views/rooms/Autocomplete.tsx", "src/components/views/rooms/RoomTile.tsx": "src/components/views/rooms/RoomTile.tsx", "src/components/views/rooms/NewRoomIntro.tsx": "src/components/views/rooms/NewRoomIntro.tsx", "src/components/views/elements/RoomName.tsx": "src/components/views/elements/RoomName.tsx", @@ -10,5 +9,6 @@ "src/components/views/avatars/BaseAvatar.tsx": "src/components/views/avatars/BaseAvatar.tsx", "src/components/views/spaces/SpacePanel.tsx": "src/components/views/spaces/SpacePanel.tsx", "src/hooks/useRoomName.ts": "src/hooks/useRoomName.ts", - "src/editor/commands.tsx": "src/editor/commands.tsx" + "src/editor/commands.tsx": "src/editor/commands.tsx", + "src/autocomplete/Autocompleter.ts": "src/autocomplete/Autocompleter.ts" } diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts new file mode 100644 index 0000000000..de058fe38e --- /dev/null +++ b/src/autocomplete/Autocompleter.ts @@ -0,0 +1,117 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017, 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from "matrix-js-sdk/src/matrix"; +import AutocompleteProvider, { ICommand } from "matrix-react-sdk/src/autocomplete/AutocompleteProvider"; +import EmojiProvider from "matrix-react-sdk/src/autocomplete/EmojiProvider"; +import NotifProvider from "matrix-react-sdk/src/autocomplete/NotifProvider"; +import RoomProvider from "matrix-react-sdk/src/autocomplete/RoomProvider"; +import SpaceProvider from "matrix-react-sdk/src/autocomplete/SpaceProvider"; +import UserProvider from "matrix-react-sdk/src/autocomplete/UserProvider"; +import { TimelineRenderingType } from "matrix-react-sdk/src/contexts/RoomContext"; +import { filterBoolean } from "matrix-react-sdk/src/utils/arrays"; +import { timeout } from "matrix-react-sdk/src/utils/promise"; +import { ReactElement } from "react"; + +export interface ISelectionRange { + beginning?: boolean; // whether the selection is in the first block of the editor or not + start: number; // byte offset relative to the start anchor of the current editor selection. + end: number; // byte offset relative to the end anchor of the current editor selection. +} + +export interface ICompletion { + type?: "at-room" | "command" | "community" | "room" | "user"; + completion: string; + completionId?: string; + component: ReactElement; + range: ISelectionRange; + command?: string; + suffix?: string; + // If provided, apply a LINK entity to the completion with the + // data = { url: href }. + href?: string; +} + +const PROVIDERS = [UserProvider, RoomProvider, EmojiProvider, NotifProvider, SpaceProvider]; + +// Providers will get rejected if they take longer than this. +const PROVIDER_COMPLETION_TIMEOUT = 3000; + +export interface IProviderCompletions { + completions: ICompletion[]; + provider: AutocompleteProvider; + command: Partial; +} + +export default class Autocompleter { + public room: Room; + public providers: AutocompleteProvider[]; + + public constructor(room: Room, renderingType: TimelineRenderingType = TimelineRenderingType.Room) { + this.room = room; + this.providers = PROVIDERS.map((Prov) => { + return new Prov(room, renderingType); + }); + } + + public destroy(): void { + this.providers.forEach((p) => { + p.destroy(); + }); + } + + public async getCompletions( + query: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { + /* Note: This intentionally waits for all providers to return, + otherwise, we run into a condition where new completions are displayed + while the user is interacting with the list, which makes it difficult + to predict whether an action will actually do what is intended + */ + // list of results from each provider, each being a list of completions or null if it times out + const completionsList: Array = await Promise.all( + this.providers.map(async (provider): Promise => { + return timeout( + provider.getCompletions(query, selection, force, limit), + null, + PROVIDER_COMPLETION_TIMEOUT, + ); + }), + ); + + // map then filter to maintain the index for the map-operation, for this.providers to line up + return filterBoolean( + completionsList.map((completions, i) => { + if (!completions || !completions.length) return; + + return { + completions, + provider: this.providers[i], + + /* the currently matched "command" the completer tried to complete + * we pass this through so that Autocomplete can figure out when to + * re-show itself once hidden. + */ + command: this.providers[i].getCurrentCommand(query, selection, force), + }; + }), + ); + } +} diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx deleted file mode 100644 index d23fd170bc..0000000000 --- a/src/components/views/rooms/Autocomplete.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; - -export const generateCompletionDomId = (n: number): string => `mx_Autocomplete_Completion_${n}`; - -export default class Autocomplete extends React.PureComponent { - public constructor(props: {} | Readonly<{}>) { - super(props); - - this.state = { - // list of completionResults, each containing completions - completions: [], - - // array of completions, so we can look up current selection by offset quickly - completionList: [], - - // how far down the completion list we are (THIS IS 1-INDEXED!) - selectionOffset: 1, - - // whether we should show completions if they're available - shouldShowCompletions: true, - - hide: false, - - forceComplete: false, - }; - } - - public render(): React.ReactNode { - return null; - } -}