Convert autocomplete stuff to TypeScript
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
37bd0f3508
commit
fced4ea51e
14 changed files with 251 additions and 142 deletions
|
@ -84,7 +84,7 @@ interface ICommandOpts {
|
|||
hideCompletionAfterSpace?: boolean;
|
||||
}
|
||||
|
||||
class Command {
|
||||
export class Command {
|
||||
command: string;
|
||||
aliases: string[];
|
||||
args: undefined | string;
|
||||
|
|
|
@ -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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
||||
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;
|
||||
}
|
|
@ -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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<IProviderCompletions[]> {
|
||||
/* 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);
|
||||
}));
|
||||
|
|
@ -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<Command>;
|
||||
|
||||
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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> {
|
||||
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 (
|
||||
<div className="mx_Autocomplete_Completion_container_block" role="listbox" aria-label={_t("Command Autocomplete")}>
|
||||
{ completions }
|
|
@ -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<Group>;
|
||||
|
||||
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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force: boolean = false): Promise<ICompletion[]> {
|
||||
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 (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/* These were earlier stateless functional components but had to be converted
|
||||
|
@ -24,7 +24,21 @@ something that is not entirely possible with stateless functional components. On
|
|||
presumably wrap them in a <div> 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<ITextualCompletionProps> {
|
||||
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 = {
|
||||
|
||||
interface IPillCompletionProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
initialComponent?: React.ReactNode,
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export class PillCompletion extends React.PureComponent<IPillCompletionProps> {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
initialComponent: PropTypes.element,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
};
|
||||
|
||||
export class PillCompletion extends React.Component {
|
||||
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,
|
||||
};
|
|
@ -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<ICompletion[]> {
|
||||
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 (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_block"
|
|
@ -17,41 +17,56 @@ 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 {PillCompletion} from './Components';
|
||||
import type {Completion, SelectionRange} from './Autocompleter';
|
||||
import {ICompletion, ISelectionRange} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import * as EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
|
||||
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
|
||||
|
||||
interface IEmoji {
|
||||
annotation: string;
|
||||
group: number;
|
||||
hexcode: string;
|
||||
order: number;
|
||||
shortcodes: string[];
|
||||
tags: string[];
|
||||
unicode: string;
|
||||
emoticon: string;
|
||||
}
|
||||
|
||||
interface IEmojiShort {
|
||||
emoji: IEmoji;
|
||||
shortname: string;
|
||||
_orderBy: number;
|
||||
}
|
||||
|
||||
// XXX: it's very unclear why we bother with this generated emojidata file.
|
||||
// all it means is that we end up bloating the bundle with precomputed stuff
|
||||
// which would be trivial to calculate and cache on demand.
|
||||
const EMOJI_SHORTNAMES = EMOJIBASE.sort((a, b) => {
|
||||
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 {
|
||||
}).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<IEmojiShort>;
|
||||
nameMatcher: QueryMatcher<IEmojiShort>;
|
||||
|
||||
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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> {
|
||||
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 (
|
||||
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
|
||||
{ completions }
|
|
@ -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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force= false): Promise<ICompletion[]> {
|
||||
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 (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
|
@ -1,4 +1,3 @@
|
|||
//@flow
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
@ -26,6 +25,13 @@ function stripDiacritics(str: string): string {
|
|||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
interface IOptions<T extends {}> {
|
||||
keys: Array<string | keyof T>;
|
||||
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<Object>, options: {[Object]: Object} = {}) {
|
||||
export default class QueryMatcher<T> {
|
||||
private _options: IOptions<T>;
|
||||
private _keys: IOptions<T>["keys"];
|
||||
private _funcs: Required<IOptions<T>["funcs"]>;
|
||||
private _items: Map<string, T[]>;
|
||||
|
||||
constructor(objects: T[], options: IOptions<T> = { keys: [] }) {
|
||||
this._options = options;
|
||||
this._keys = options.keys;
|
||||
this._funcs = options.funcs || [];
|
||||
|
@ -60,7 +71,7 @@ export default class QueryMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
setObjects(objects: Array<Object>) {
|
||||
setObjects(objects: T[]) {
|
||||
this._items = new Map();
|
||||
|
||||
for (const object of objects) {
|
||||
|
@ -81,7 +92,7 @@ export default class QueryMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
match(query: String): Array<Object> {
|
||||
match(query: string): T[] {
|
||||
query = stripDiacritics(query).toLowerCase();
|
||||
if (this._options.shouldMatchWordsOnly) {
|
||||
query = query.replace(/[^\w]/g, '');
|
|
@ -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<Room>;
|
||||
|
||||
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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
||||
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 (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
|
@ -1,4 +1,3 @@
|
|||
//@flow
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
@ -18,7 +17,7 @@ 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 {PillCompletion} from './Components';
|
||||
|
@ -27,9 +26,13 @@ import QueryMatcher from './QueryMatcher';
|
|||
import _sortBy from 'lodash/sortBy';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
|
||||
import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk';
|
||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import RoomMember from "matrix-js-sdk/src/models/room-member";
|
||||
import RoomState from "matrix-js-sdk/src/models/room-state";
|
||||
import EventTimeline from "matrix-js-sdk/src/models/event-timeline";
|
||||
import {makeUserPermalink} from "../utils/permalinks/Permalinks";
|
||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
|
||||
const USER_REGEX = /\B@\S*/g;
|
||||
|
||||
|
@ -37,9 +40,15 @@ const USER_REGEX = /\B@\S*/g;
|
|||
// to allow you to tab-complete /mat into /(matthew)
|
||||
const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g;
|
||||
|
||||
interface IRoomTimelineData {
|
||||
timeline: EventTimeline;
|
||||
liveEvent?: boolean;
|
||||
}
|
||||
|
||||
export default class UserProvider extends AutocompleteProvider {
|
||||
users: Array<RoomMember> = null;
|
||||
room: Room = null;
|
||||
matcher: QueryMatcher<RoomMember>;
|
||||
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<Completion> {
|
||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
||||
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 (
|
||||
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
|
||||
{ completions }
|
|
@ -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<IProps, IState> {
|
||||
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<HTMLDivElement>;
|
||||
|
||||
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 ? (
|
||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||
<div className="mx_Autocomplete" ref={this.container}>
|
||||
{ renderedCompletions }
|
||||
</div>
|
||||
) : 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),
|
||||
};
|
|
@ -2,6 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2016",
|
||||
|
|
Loading…
Reference in a new issue