diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 5e8c1087f7..6db683d339 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -84,7 +84,7 @@ interface ICommandOpts { hideCompletionAfterSpace?: boolean; } -class Command { +export class Command { command: string; aliases: string[]; args: undefined | string; diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.tsx similarity index 82% rename from src/autocomplete/AutocompleteProvider.js rename to src/autocomplete/AutocompleteProvider.tsx index 98ae83c526..7efe823250 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -16,10 +16,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import type {Completion, SelectionRange} from './Autocompleter'; +import * as React from 'react'; +import type {ICompletion, ISelectionRange} from './Autocompleter'; + +export interface ICommand { + command: string | null; + range: { + start: number; + end: number; + }; +} export default class AutocompleteProvider { + commandRegex: RegExp; + forcedCommandRegex: RegExp; + constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { if (commandRegex) { if (!commandRegex.global) { @@ -42,11 +53,11 @@ export default class AutocompleteProvider { /** * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. * @param {string} query The query string - * @param {SelectionRange} selection Selection to search + * @param {ISelectionRange} selection Selection to search * @param {boolean} force True if the user is forcing completion * @return {object} { command, range } where both objects fields are null if no match */ - getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false) { + getCurrentCommand(query: string, selection: ISelectionRange, force = false) { let commandRegex = this.commandRegex; if (force && this.shouldForceComplete()) { @@ -82,7 +93,7 @@ export default class AutocompleteProvider { }; } - async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { return []; } @@ -90,7 +101,7 @@ export default class AutocompleteProvider { return 'Default Provider'; } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode | null { console.error('stub; should be implemented in subclasses'); return null; } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.ts similarity index 70% rename from src/autocomplete/Autocompleter.js rename to src/autocomplete/Autocompleter.ts index a26eb6033b..8384eb9d4f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.ts @@ -15,10 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// @flow - -import type {Component} from 'react'; -import {Room} from 'matrix-js-sdk'; +import {ReactElement} from 'react'; +import Room from 'matrix-js-sdk/src/models/room'; import CommandProvider from './CommandProvider'; import CommunityProvider from './CommunityProvider'; import DuckDuckGoProvider from './DuckDuckGoProvider'; @@ -27,22 +25,26 @@ import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; import {timeout} from "../utils/promise"; +import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; -export type SelectionRange = { - 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 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 type Completion = { +export interface ICompletion { + type: "at-room" | "command" | "community" | "room" | "user"; completion: string, - component: ?Component, - range: SelectionRange, - command: ?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, -}; + href?: string, +} const PROVIDERS = [ UserProvider, @@ -57,7 +59,16 @@ const PROVIDERS = [ // Providers will get rejected if they take longer than this. const PROVIDER_COMPLETION_TIMEOUT = 3000; +export interface IProviderCompletions { + completions: ICompletion[]; + provider: AutocompleteProvider; + command: ICommand; +} + export default class Autocompleter { + room: Room; + providers: AutocompleteProvider[]; + constructor(room: Room) { this.room = room; this.providers = PROVIDERS.map((Prov) => { @@ -71,13 +82,14 @@ export default class Autocompleter { }); } - async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + async getCompletions(query: string, selection: ISelectionRange, force = false): 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 */ - const completionsList = await Promise.all(this.providers.map(provider => { + // list of results from each provider, each being a list of completions or null if it times out + const completionsList: ICompletion[][] = await Promise.all(this.providers.map(provider => { return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT); })); diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.tsx similarity index 89% rename from src/autocomplete/CommandProvider.js rename to src/autocomplete/CommandProvider.tsx index 0b8af4d6f9..b9e8975491 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.tsx @@ -17,17 +17,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import * as React from 'react'; import {_t} from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import QueryMatcher from './QueryMatcher'; import {TextualCompletion} from './Components'; -import type {Completion, SelectionRange} from "./Autocompleter"; -import {Commands, CommandMap} from '../SlashCommands'; +import {ICompletion, ISelectionRange} from "./Autocompleter"; +import {Command, Commands, CommandMap} from '../SlashCommands'; const COMMAND_RE = /(^\/\w*)(?: .*)?/g; export default class CommandProvider extends AutocompleteProvider { + matcher: QueryMatcher; + constructor() { super(COMMAND_RE); this.matcher = new QueryMatcher(Commands, { @@ -36,7 +38,7 @@ export default class CommandProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array { + async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise { const {command, range} = this.getCurrentCommand(query, selection); if (!command) return []; @@ -85,7 +87,7 @@ export default class CommandProvider extends AutocompleteProvider { return '*️⃣ ' + _t('Commands'); } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode { return (
{ completions } diff --git a/src/autocomplete/CommunityProvider.js b/src/autocomplete/CommunityProvider.tsx similarity index 91% rename from src/autocomplete/CommunityProvider.js rename to src/autocomplete/CommunityProvider.tsx index b863603aae..f1a5c0f2ca 100644 --- a/src/autocomplete/CommunityProvider.js +++ b/src/autocomplete/CommunityProvider.tsx @@ -15,7 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import * as React from 'react'; +import Group from "matrix-js-sdk/src/models/group"; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {MatrixClientPeg} from '../MatrixClientPeg'; @@ -24,7 +25,7 @@ import {PillCompletion} from './Components'; import * as sdk from '../index'; import _sortBy from 'lodash/sortBy'; import {makeGroupPermalink} from "../utils/permalinks/Permalinks"; -import type {Completion, SelectionRange} from "./Autocompleter"; +import {ICompletion, ISelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; const COMMUNITY_REGEX = /\B\+\S*/g; @@ -39,6 +40,8 @@ function score(query, space) { } export default class CommunityProvider extends AutocompleteProvider { + matcher: QueryMatcher; + constructor() { super(COMMUNITY_REGEX); this.matcher = new QueryMatcher([], { @@ -46,7 +49,7 @@ export default class CommunityProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + async getCompletions(query: string, selection: ISelectionRange, force: boolean = false): Promise { const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); // Disable autocompletions when composing commands because of various issues @@ -104,7 +107,7 @@ export default class CommunityProvider extends AutocompleteProvider { return '💬 ' + _t('Communities'); } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode { return (
before rendering but I think this is the better way to do it. */ -export class TextualCompletion extends React.Component { +interface ITextualCompletionProps { + title?: string; + subtitle?: string; + description?: string; + className?: string; +} + +export class TextualCompletion extends React.PureComponent { + static propTypes = { + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + className: PropTypes.string, + }; + render() { const { title, @@ -42,14 +56,24 @@ export class TextualCompletion extends React.Component { ); } } -TextualCompletion.propTypes = { - title: PropTypes.string, - subtitle: PropTypes.string, - description: PropTypes.string, - className: PropTypes.string, -}; -export class PillCompletion extends React.Component { +interface IPillCompletionProps { + title?: string; + subtitle?: string; + description?: string; + initialComponent?: React.ReactNode, + className?: string; +} + +export class PillCompletion extends React.PureComponent { + static propTypes = { + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + initialComponent: PropTypes.element, + className: PropTypes.string, + }; + render() { const { title, @@ -69,10 +93,3 @@ export class PillCompletion extends React.Component { ); } } -PillCompletion.propTypes = { - title: PropTypes.string, - subtitle: PropTypes.string, - description: PropTypes.string, - initialComponent: PropTypes.element, - className: PropTypes.string, -}; diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.tsx similarity index 90% rename from src/autocomplete/DuckDuckGoProvider.js rename to src/autocomplete/DuckDuckGoProvider.tsx index 8cff83554a..5a024d956c 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.tsx @@ -16,12 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import * as React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {TextualCompletion} from './Components'; -import type {SelectionRange} from "./Autocompleter"; +import {ICompletion, ISelectionRange} from "./Autocompleter"; const DDG_REGEX = /\/ddg\s+(.+)$/g; const REFERRER = 'vector'; @@ -31,12 +31,12 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { super(DDG_REGEX); } - static getQueryUri(query: String) { + static getQueryUri(query: string) { return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}` + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; } - async getCompletions(query: string, selection: SelectionRange, force: boolean = false) { + async getCompletions(query: string, selection: ISelectionRange, force= false): Promise { const {command, range} = this.getCurrentCommand(query, selection); if (!query || !command) { return []; @@ -95,7 +95,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { return '🔍 ' + _t('Results from DuckDuckGo'); } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode { return (
{ +const EMOJI_SHORTNAMES: IEmojiShort[] = (EMOJIBASE as IEmoji[]).sort((a, b) => { if (a.group === b.group) { return a.order - b.order; } return a.group - b.group; -}).map((emoji, index) => { - return { - emoji, - shortname: `:${emoji.shortcodes[0]}:`, - // Include the index so that we can preserve the original order - _orderBy: index, - }; -}); +}).map((emoji, index) => ({ + emoji, + shortname: `:${emoji.shortcodes[0]}:`, + // Include the index so that we can preserve the original order + _orderBy: index, +})); function score(query, space) { const index = space.indexOf(query); @@ -63,6 +78,9 @@ function score(query, space) { } export default class EmojiProvider extends AutocompleteProvider { + matcher: QueryMatcher; + nameMatcher: QueryMatcher; + constructor() { super(EMOJI_REGEX); this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { @@ -80,7 +98,7 @@ export default class EmojiProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array { + async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } @@ -132,7 +150,7 @@ export default class EmojiProvider extends AutocompleteProvider { return '😃 ' + _t('Emoji'); } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode { return (
{ completions } diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.tsx similarity index 86% rename from src/autocomplete/NotifProvider.js rename to src/autocomplete/NotifProvider.tsx index e7c8f6f70d..b01f52fd23 100644 --- a/src/autocomplete/NotifProvider.js +++ b/src/autocomplete/NotifProvider.tsx @@ -14,23 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import * as React from 'react'; +import Room from "matrix-js-sdk/src/models/room"; import AutocompleteProvider from './AutocompleteProvider'; import { _t } from '../languageHandler'; import {MatrixClientPeg} from '../MatrixClientPeg'; import {PillCompletion} from './Components'; import * as sdk from '../index'; -import type {Completion, SelectionRange} from "./Autocompleter"; +import {ICompletion, ISelectionRange} from "./Autocompleter"; const AT_ROOM_REGEX = /@\S*/g; export default class NotifProvider extends AutocompleteProvider { + room: Room; + constructor(room) { super(AT_ROOM_REGEX); this.room = room; } - async getCompletions(query: string, selection: SelectionRange, force:boolean = false): Array { + async getCompletions(query: string, selection: ISelectionRange, force= false): Promise { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const client = MatrixClientPeg.get(); @@ -57,7 +60,7 @@ export default class NotifProvider extends AutocompleteProvider { return '❗️ ' + _t('Room Notification'); } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode { return (
@@ -26,6 +25,13 @@ function stripDiacritics(str: string): string { return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } +interface IOptions { + keys: Array; + funcs?: Array<(T) => string>; + shouldMatchWordsOnly?: boolean; + shouldMatchPrefix?: boolean; +} + /** * Simple search matcher that matches any results with the query string anywhere * in the search string. Returns matches in the order the query string appears @@ -39,8 +45,13 @@ function stripDiacritics(str: string): string { * @param {function[]} options.funcs List of functions that when called with the * object as an arg will return a string to use as an index */ -export default class QueryMatcher { - constructor(objects: Array, options: {[Object]: Object} = {}) { +export default class QueryMatcher { + private _options: IOptions; + private _keys: IOptions["keys"]; + private _funcs: Required["funcs"]>; + private _items: Map; + + constructor(objects: T[], options: IOptions = { keys: [] }) { this._options = options; this._keys = options.keys; this._funcs = options.funcs || []; @@ -60,7 +71,7 @@ export default class QueryMatcher { } } - setObjects(objects: Array) { + setObjects(objects: T[]) { this._items = new Map(); for (const object of objects) { @@ -81,7 +92,7 @@ export default class QueryMatcher { } } - match(query: String): Array { + match(query: string): T[] { query = stripDiacritics(query).toLowerCase(); if (this._options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.tsx similarity index 92% rename from src/autocomplete/RoomProvider.js rename to src/autocomplete/RoomProvider.tsx index a0f670e769..6d98b37318 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.tsx @@ -17,7 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import * as React from 'react'; +import Room from "matrix-js-sdk/src/models/room"; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {MatrixClientPeg} from '../MatrixClientPeg'; @@ -26,7 +27,7 @@ import {PillCompletion} from './Components'; import * as sdk from '../index'; import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; -import type {Completion, SelectionRange} from "./Autocompleter"; +import {ICompletion, ISelectionRange} from "./Autocompleter"; const ROOM_REGEX = /\B#\S*/g; @@ -48,6 +49,8 @@ function matcherObject(room, displayedAlias, matchName = "") { } export default class RoomProvider extends AutocompleteProvider { + matcher: QueryMatcher; + constructor() { super(ROOM_REGEX); this.matcher = new QueryMatcher([], { @@ -55,7 +58,7 @@ export default class RoomProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const client = MatrixClientPeg.get(); @@ -115,7 +118,7 @@ export default class RoomProvider extends AutocompleteProvider { return '💬 ' + _t('Rooms'); } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode { return (
= null; - room: Room = null; + matcher: QueryMatcher; + users: RoomMember[]; + room: Room; constructor(room: Room) { super(USER_REGEX, FORCED_USER_REGEX); @@ -51,21 +60,19 @@ export default class UserProvider extends AutocompleteProvider { shouldMatchWordsOnly: false, }); - this._onRoomTimelineBound = this._onRoomTimeline.bind(this); - this._onRoomStateMemberBound = this._onRoomStateMember.bind(this); - - MatrixClientPeg.get().on("Room.timeline", this._onRoomTimelineBound); - MatrixClientPeg.get().on("RoomState.members", this._onRoomStateMemberBound); + MatrixClientPeg.get().on("Room.timeline", this._onRoomTimeline); + MatrixClientPeg.get().on("RoomState.members", this._onRoomStateMember); } destroy() { if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); - MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); + MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimeline); + MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMember); } } - _onRoomTimeline(ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: Object) { + _onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, + data: IRoomTimelineData) => { if (!room) return; if (removed) return; if (room.roomId !== this.room.roomId) return; @@ -79,9 +86,9 @@ export default class UserProvider extends AutocompleteProvider { // TODO: lazyload if we have no ev.sender room member? this.onUserSpoke(ev.sender); - } + }; - _onRoomStateMember(ev: MatrixEvent, state: RoomState, member: RoomMember) { + _onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => { // ignore members in other rooms if (member.roomId !== this.room.roomId) { return; @@ -89,9 +96,9 @@ export default class UserProvider extends AutocompleteProvider { // blow away the users cache this.users = null; - } + }; - async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); // lazy-load user list into matcher @@ -163,7 +170,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher.setObjects(this.users); } - renderCompletions(completions: [React.Component]): ?React.Component { + renderCompletions(completions: React.ReactNode[]): React.ReactNode { return (
{ completions } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.tsx similarity index 80% rename from src/components/views/rooms/Autocomplete.js rename to src/components/views/rooms/Autocomplete.tsx index 76a3a19e00..d4fcc5852b 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.tsx @@ -15,30 +15,62 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as PropTypes from 'prop-types'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; -import type {Completion} from '../../../autocomplete/Autocompleter'; -import { Room } from 'matrix-js-sdk'; +import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter'; +import {Room} from 'matrix-js-sdk/src/models/room'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; -import {sleep} from "../../../utils/promise"; const COMPOSER_SELECTED = 0; export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`; -export default class Autocomplete extends React.Component { +interface IProps { + query: string; + onConfirm: (ICompletion) => void; + onSelectionChange?: (ICompletion, number) => void; + selection: ISelectionRange; + room: Room; +} + +interface IState { + completions: IProviderCompletions[]; + completionList: ICompletion[]; + selectionOffset: number; + shouldShowCompletions: boolean; + hide: boolean; + forceComplete: boolean; +} + +export default class Autocomplete extends React.PureComponent { + static propTypes = { + // the query string for which to show autocomplete suggestions + query: PropTypes.string.isRequired, + + // method invoked with range and text content when completion is confirmed + onConfirm: PropTypes.func.isRequired, + + // method invoked when selected (if any) completion changes + onSelectionChange: PropTypes.func, + + // The room in which we're autocompleting + room: PropTypes.instanceOf(Room), + }; + + autocompleter: Autocompleter; + queryRequested: string; + debounceCompletionsRequest: NodeJS.Timeout; + container: React.RefObject; + constructor(props) { super(props); this.autocompleter = new Autocompleter(props.room); - this.completionPromise = null; - this.hide = this.hide.bind(this); - this.onCompletionClicked = this.onCompletionClicked.bind(this); this.state = { // list of completionResults, each containing completions @@ -57,13 +89,15 @@ export default class Autocomplete extends React.Component { forceComplete: false, }; + + this.container = React.createRef(); } componentDidMount() { this._applyNewProps(); } - _applyNewProps(oldQuery, oldRoom) { + _applyNewProps(oldQuery?: string, oldRoom?: Room) { if (oldRoom && this.props.room.roomId !== oldRoom.roomId) { this.autocompleter.destroy(); this.autocompleter = new Autocompleter(this.props.room); @@ -159,7 +193,7 @@ export default class Autocomplete extends React.Component { }); } - hasSelection(): bool { + hasSelection(): boolean { return this.countCompletions() > 0 && this.state.selectionOffset !== 0; } @@ -168,7 +202,7 @@ export default class Autocomplete extends React.Component { } // called from MessageComposerInput - moveSelection(delta): ?Completion { + moveSelection(delta): ICompletion | undefined { const completionCount = this.countCompletions(); if (completionCount === 0) return; // there are no items to move the selection through @@ -190,9 +224,14 @@ export default class Autocomplete extends React.Component { this.hide(); } - hide() { - this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []}); - } + hide = () => { + this.setState({ + hide: true, + selectionOffset: 0, + completions: [], + completionList: [], + }); + }; forceComplete() { return new Promise((resolve) => { @@ -207,7 +246,7 @@ export default class Autocomplete extends React.Component { }); } - onCompletionClicked(selectionOffset: number): boolean { + onCompletionClicked = (selectionOffset: number): boolean => { if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) { return false; } @@ -216,7 +255,7 @@ export default class Autocomplete extends React.Component { this.hide(); return true; - } + }; setSelection(selectionOffset: number) { this.setState({selectionOffset, hide: false}); @@ -229,20 +268,16 @@ export default class Autocomplete extends React.Component { this._applyNewProps(prevProps.query, prevProps.room); // this is the selected completion, so scroll it into view if needed const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`]; - if (selectedCompletion && this.container) { + if (selectedCompletion && this.container.current) { const domNode = ReactDOM.findDOMNode(selectedCompletion); const offsetTop = domNode && domNode.offsetTop; - if (offsetTop > this.container.scrollTop + this.container.offsetHeight || - offsetTop < this.container.scrollTop) { - this.container.scrollTop = offsetTop - this.container.offsetTop; + if (offsetTop > this.container.current.scrollTop + this.container.current.offsetHeight || + offsetTop < this.container.current.scrollTop) { + this.container.current.scrollTop = offsetTop - this.container.current.offsetTop; } } } - setState(state, func) { - super.setState(state, func); - } - render() { let position = 1; const renderedCompletions = this.state.completions.map((completionResult, i) => { @@ -276,23 +311,9 @@ export default class Autocomplete extends React.Component { }).filter((completion) => !!completion); return !this.state.hide && renderedCompletions.length > 0 ? ( -
this.container = e}> +
{ renderedCompletions }
) : null; } } - -Autocomplete.propTypes = { - // the query string for which to show autocomplete suggestions - query: PropTypes.string.isRequired, - - // method invoked with range and text content when completion is confirmed - onConfirm: PropTypes.func.isRequired, - - // method invoked when selected (if any) completion changes - onSelectionChange: PropTypes.func, - - // The room in which we're autocompleting - room: PropTypes.instanceOf(Room), -}; diff --git a/tsconfig.json b/tsconfig.json index d70e0a85f0..dc61d72419 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true, + "resolveJsonModule": true, "module": "commonjs", "moduleResolution": "node", "target": "es2016",