Merge pull request #4452 from matrix-org/t3chguy/autocomplete
Convert autocomplete stuff to TypeScript
This commit is contained in:
commit
dd1f1b3092
15 changed files with 239 additions and 152 deletions
|
@ -86,7 +86,7 @@ interface ICommandOpts {
|
||||||
hideCompletionAfterSpace?: boolean;
|
hideCompletionAfterSpace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Command {
|
export class Command {
|
||||||
command: string;
|
command: string;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
args: undefined | string;
|
args: undefined | string;
|
||||||
|
|
|
@ -17,9 +17,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type {Completion, SelectionRange} from './Autocompleter';
|
import type {ICompletion, ISelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
|
export interface ICommand {
|
||||||
|
command: string | null;
|
||||||
|
range: {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
|
commandRegex: RegExp;
|
||||||
|
forcedCommandRegex: RegExp;
|
||||||
|
|
||||||
constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) {
|
constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) {
|
||||||
if (commandRegex) {
|
if (commandRegex) {
|
||||||
if (!commandRegex.global) {
|
if (!commandRegex.global) {
|
||||||
|
@ -42,25 +53,25 @@ export default class AutocompleteProvider {
|
||||||
/**
|
/**
|
||||||
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
* 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 {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
|
* @param {boolean} force True if the user is forcing completion
|
||||||
* @return {object} { command, range } where both objects fields are null if no match
|
* @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;
|
let commandRegex = this.commandRegex;
|
||||||
|
|
||||||
if (force && this.shouldForceComplete()) {
|
if (force && this.shouldForceComplete()) {
|
||||||
commandRegex = this.forcedCommandRegex || /\S+/g;
|
commandRegex = this.forcedCommandRegex || /\S+/g;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandRegex == null) {
|
if (commandRegex === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
commandRegex.lastIndex = 0;
|
commandRegex.lastIndex = 0;
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
while ((match = commandRegex.exec(query)) != null) {
|
while ((match = commandRegex.exec(query)) !== null) {
|
||||||
const start = match.index;
|
const start = match.index;
|
||||||
const end = start + match[0].length;
|
const end = start + match[0].length;
|
||||||
if (selection.start <= end && selection.end >= start) {
|
if (selection.start <= end && selection.end >= start) {
|
||||||
|
@ -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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +101,7 @@ export default class AutocompleteProvider {
|
||||||
return 'Default Provider';
|
return 'Default Provider';
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode | null {
|
||||||
console.error('stub; should be implemented in subclasses');
|
console.error('stub; should be implemented in subclasses');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
|
@ -15,10 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
import {ReactElement} from 'react';
|
||||||
|
import Room from 'matrix-js-sdk/src/models/room';
|
||||||
import type {Component} from 'react';
|
|
||||||
import {Room} from 'matrix-js-sdk';
|
|
||||||
import CommandProvider from './CommandProvider';
|
import CommandProvider from './CommandProvider';
|
||||||
import CommunityProvider from './CommunityProvider';
|
import CommunityProvider from './CommunityProvider';
|
||||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
|
@ -27,22 +25,26 @@ import UserProvider from './UserProvider';
|
||||||
import EmojiProvider from './EmojiProvider';
|
import EmojiProvider from './EmojiProvider';
|
||||||
import NotifProvider from './NotifProvider';
|
import NotifProvider from './NotifProvider';
|
||||||
import {timeout} from "../utils/promise";
|
import {timeout} from "../utils/promise";
|
||||||
|
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
||||||
|
|
||||||
export type SelectionRange = {
|
export interface ISelectionRange {
|
||||||
beginning: boolean, // whether the selection is in the first block of the editor or not
|
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.
|
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.
|
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,
|
completion: string,
|
||||||
component: ?Component,
|
completionId?: string;
|
||||||
range: SelectionRange,
|
component?: ReactElement,
|
||||||
command: ?string,
|
range: ISelectionRange,
|
||||||
|
command?: string,
|
||||||
|
suffix?: string;
|
||||||
// If provided, apply a LINK entity to the completion with the
|
// If provided, apply a LINK entity to the completion with the
|
||||||
// data = { url: href }.
|
// data = { url: href }.
|
||||||
href: ?string,
|
href?: string,
|
||||||
};
|
}
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
UserProvider,
|
UserProvider,
|
||||||
|
@ -57,7 +59,16 @@ const PROVIDERS = [
|
||||||
// Providers will get rejected if they take longer than this.
|
// Providers will get rejected if they take longer than this.
|
||||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
export interface IProviderCompletions {
|
||||||
|
completions: ICompletion[];
|
||||||
|
provider: AutocompleteProvider;
|
||||||
|
command: ICommand;
|
||||||
|
}
|
||||||
|
|
||||||
export default class Autocompleter {
|
export default class Autocompleter {
|
||||||
|
room: Room;
|
||||||
|
providers: AutocompleteProvider[];
|
||||||
|
|
||||||
constructor(room: Room) {
|
constructor(room: Room) {
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.providers = PROVIDERS.map((Prov) => {
|
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,
|
/* Note: This intentionally waits for all providers to return,
|
||||||
otherwise, we run into a condition where new completions are displayed
|
otherwise, we run into a condition where new completions are displayed
|
||||||
while the user is interacting with the list, which makes it difficult
|
while the user is interacting with the list, which makes it difficult
|
||||||
to predict whether an action will actually do what is intended
|
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);
|
return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -22,12 +22,14 @@ import {_t} from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
import {Commands, CommandMap} from '../SlashCommands';
|
import {Command, Commands, CommandMap} from '../SlashCommands';
|
||||||
|
|
||||||
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
|
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
|
||||||
|
|
||||||
export default class CommandProvider extends AutocompleteProvider {
|
export default class CommandProvider extends AutocompleteProvider {
|
||||||
|
matcher: QueryMatcher<Command>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(COMMAND_RE);
|
super(COMMAND_RE);
|
||||||
this.matcher = new QueryMatcher(Commands, {
|
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);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!command) return [];
|
if (!command) return [];
|
||||||
|
|
||||||
|
@ -85,7 +87,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
return '*️⃣ ' + _t('Commands');
|
return '*️⃣ ' + _t('Commands');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_Autocomplete_Completion_container_block" role="listbox" aria-label={_t("Command Autocomplete")}>
|
<div className="mx_Autocomplete_Completion_container_block" role="listbox" aria-label={_t("Command Autocomplete")}>
|
||||||
{ completions }
|
{ completions }
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Group from "matrix-js-sdk/src/models/group";
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
|
@ -24,7 +25,7 @@ import {PillCompletion} from './Components';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
import FlairStore from "../stores/FlairStore";
|
import FlairStore from "../stores/FlairStore";
|
||||||
|
|
||||||
const COMMUNITY_REGEX = /\B\+\S*/g;
|
const COMMUNITY_REGEX = /\B\+\S*/g;
|
||||||
|
@ -39,6 +40,8 @@ function score(query, space) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CommunityProvider extends AutocompleteProvider {
|
export default class CommunityProvider extends AutocompleteProvider {
|
||||||
|
matcher: QueryMatcher<Group>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(COMMUNITY_REGEX);
|
super(COMMUNITY_REGEX);
|
||||||
this.matcher = new QueryMatcher([], {
|
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 = false): Promise<ICompletion[]> {
|
||||||
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||||
|
|
||||||
// Disable autocompletions when composing commands because of various issues
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
@ -104,7 +107,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
||||||
return '💬 ' + _t('Communities');
|
return '💬 ' + _t('Communities');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
/* These were earlier stateless functional components but had to be converted
|
/* These were earlier stateless functional components but had to be converted
|
||||||
|
@ -24,7 +23,14 @@ 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.
|
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> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
|
@ -42,14 +48,16 @@ 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<IPillCompletionProps> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
|
@ -69,10 +77,3 @@ export class PillCompletion extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PillCompletion.propTypes = {
|
|
||||||
title: PropTypes.string,
|
|
||||||
subtitle: PropTypes.string,
|
|
||||||
description: PropTypes.string,
|
|
||||||
initialComponent: PropTypes.element,
|
|
||||||
className: PropTypes.string,
|
|
||||||
};
|
|
|
@ -21,7 +21,7 @@ import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
|
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
import type {SelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||||
const REFERRER = 'vector';
|
const REFERRER = 'vector';
|
||||||
|
@ -31,12 +31,12 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
super(DDG_REGEX);
|
super(DDG_REGEX);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getQueryUri(query: String) {
|
static getQueryUri(query: string) {
|
||||||
return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
|
return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
|
||||||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
+ `&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);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!query || !command) {
|
if (!query || !command) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -95,7 +95,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
return '🔍 ' + _t('Results from DuckDuckGo');
|
return '🔍 ' + _t('Results from DuckDuckGo');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_Autocomplete_Completion_container_block"
|
className="mx_Autocomplete_Completion_container_block"
|
|
@ -22,36 +22,37 @@ import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import type {Completion, SelectionRange} from './Autocompleter';
|
import {ICompletion, ISelectionRange} from './Autocompleter';
|
||||||
import _uniq from 'lodash/uniq';
|
import _uniq from 'lodash/uniq';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||||
|
import { EMOJI, IEmoji } from '../emoji';
|
||||||
|
|
||||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
|
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
|
||||||
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
|
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
|
||||||
|
|
||||||
// XXX: it's very unclear why we bother with this generated emojidata file.
|
interface IEmojiShort {
|
||||||
// all it means is that we end up bloating the bundle with precomputed stuff
|
emoji: IEmoji;
|
||||||
// which would be trivial to calculate and cache on demand.
|
shortname: string;
|
||||||
const EMOJI_SHORTNAMES = EMOJIBASE.sort((a, b) => {
|
_orderBy: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => {
|
||||||
if (a.group === b.group) {
|
if (a.group === b.group) {
|
||||||
return a.order - b.order;
|
return a.order - b.order;
|
||||||
}
|
}
|
||||||
return a.group - b.group;
|
return a.group - b.group;
|
||||||
}).map((emoji, index) => {
|
}).map((emoji, index) => ({
|
||||||
return {
|
|
||||||
emoji,
|
emoji,
|
||||||
shortname: `:${emoji.shortcodes[0]}:`,
|
shortname: `:${emoji.shortcodes[0]}:`,
|
||||||
// Include the index so that we can preserve the original order
|
// Include the index so that we can preserve the original order
|
||||||
_orderBy: index,
|
_orderBy: index,
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
function score(query, space) {
|
function score(query, space) {
|
||||||
const index = space.indexOf(query);
|
const index = space.indexOf(query);
|
||||||
|
@ -63,6 +64,9 @@ function score(query, space) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EmojiProvider extends AutocompleteProvider {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
|
matcher: QueryMatcher<IEmojiShort>;
|
||||||
|
nameMatcher: QueryMatcher<IEmojiShort>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
super(EMOJI_REGEX);
|
||||||
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
||||||
|
@ -80,7 +84,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")) {
|
if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) {
|
||||||
return []; // don't give any suggestions if the user doesn't want them
|
return []; // don't give any suggestions if the user doesn't want them
|
||||||
}
|
}
|
||||||
|
@ -132,7 +136,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
return '😃 ' + _t('Emoji');
|
return '😃 ' + _t('Emoji');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
|
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
|
||||||
{ completions }
|
{ completions }
|
|
@ -15,22 +15,25 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Room from "matrix-js-sdk/src/models/room";
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const AT_ROOM_REGEX = /@\S*/g;
|
const AT_ROOM_REGEX = /@\S*/g;
|
||||||
|
|
||||||
export default class NotifProvider extends AutocompleteProvider {
|
export default class NotifProvider extends AutocompleteProvider {
|
||||||
|
room: Room;
|
||||||
|
|
||||||
constructor(room) {
|
constructor(room) {
|
||||||
super(AT_ROOM_REGEX);
|
super(AT_ROOM_REGEX);
|
||||||
this.room = room;
|
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 RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
@ -57,7 +60,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||||
return '❗️ ' + _t('Room Notification');
|
return '❗️ ' + _t('Room Notification');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
|
@ -1,4 +1,3 @@
|
||||||
//@flow
|
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Aviral Dasgupta
|
Copyright 2017 Aviral Dasgupta
|
||||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
@ -26,6 +25,13 @@ function stripDiacritics(str: string): string {
|
||||||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
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
|
* 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
|
* 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
|
* @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
|
* object as an arg will return a string to use as an index
|
||||||
*/
|
*/
|
||||||
export default class QueryMatcher {
|
export default class QueryMatcher<T> {
|
||||||
constructor(objects: Array<Object>, options: {[Object]: Object} = {}) {
|
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._options = options;
|
||||||
this._keys = options.keys;
|
this._keys = options.keys;
|
||||||
this._funcs = options.funcs || [];
|
this._funcs = options.funcs || [];
|
||||||
|
@ -60,7 +71,7 @@ export default class QueryMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setObjects(objects: Array<Object>) {
|
setObjects(objects: T[]) {
|
||||||
this._items = new Map();
|
this._items = new Map();
|
||||||
|
|
||||||
for (const object of objects) {
|
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();
|
query = stripDiacritics(query).toLowerCase();
|
||||||
if (this._options.shouldMatchWordsOnly) {
|
if (this._options.shouldMatchWordsOnly) {
|
||||||
query = query.replace(/[^\w]/g, '');
|
query = query.replace(/[^\w]/g, '');
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Room from "matrix-js-sdk/src/models/room";
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
|
@ -26,11 +27,11 @@ import {PillCompletion} from './Components';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const ROOM_REGEX = /\B#\S*/g;
|
const ROOM_REGEX = /\B#\S*/g;
|
||||||
|
|
||||||
function score(query, space) {
|
function score(query: string, space: string) {
|
||||||
const index = space.indexOf(query);
|
const index = space.indexOf(query);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
return Infinity;
|
return Infinity;
|
||||||
|
@ -39,7 +40,7 @@ function score(query, space) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function matcherObject(room, displayedAlias, matchName = "") {
|
function matcherObject(room: Room, displayedAlias: string, matchName = "") {
|
||||||
return {
|
return {
|
||||||
room,
|
room,
|
||||||
matchName,
|
matchName,
|
||||||
|
@ -48,6 +49,8 @@ function matcherObject(room, displayedAlias, matchName = "") {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomProvider extends AutocompleteProvider {
|
export default class RoomProvider extends AutocompleteProvider {
|
||||||
|
matcher: QueryMatcher<Room>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ROOM_REGEX);
|
super(ROOM_REGEX);
|
||||||
this.matcher = new QueryMatcher([], {
|
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 RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
@ -115,7 +118,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
return '💬 ' + _t('Rooms');
|
return '💬 ' + _t('Rooms');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
|
@ -1,4 +1,3 @@
|
||||||
//@flow
|
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
@ -27,9 +26,13 @@ import QueryMatcher from './QueryMatcher';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
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 {makeUserPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const USER_REGEX = /\B@\S*/g;
|
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)
|
// to allow you to tab-complete /mat into /(matthew)
|
||||||
const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g;
|
const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g;
|
||||||
|
|
||||||
|
interface IRoomTimelineData {
|
||||||
|
timeline: EventTimeline;
|
||||||
|
liveEvent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default class UserProvider extends AutocompleteProvider {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
users: Array<RoomMember> = null;
|
matcher: QueryMatcher<RoomMember>;
|
||||||
room: Room = null;
|
users: RoomMember[];
|
||||||
|
room: Room;
|
||||||
|
|
||||||
constructor(room: Room) {
|
constructor(room: Room) {
|
||||||
super(USER_REGEX, FORCED_USER_REGEX);
|
super(USER_REGEX, FORCED_USER_REGEX);
|
||||||
|
@ -51,21 +60,19 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
shouldMatchWordsOnly: false,
|
shouldMatchWordsOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
this._onRoomStateMemberBound = this._onRoomStateMember.bind(this);
|
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||||
|
|
||||||
MatrixClientPeg.get().on("Room.timeline", this._onRoomTimelineBound);
|
|
||||||
MatrixClientPeg.get().on("RoomState.members", this._onRoomStateMemberBound);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomTimeline(ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: Object) {
|
private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean,
|
||||||
|
data: IRoomTimelineData) => {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
if (room.roomId !== this.room.roomId) 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?
|
// TODO: lazyload if we have no ev.sender room member?
|
||||||
this.onUserSpoke(ev.sender);
|
this.onUserSpoke(ev.sender);
|
||||||
}
|
};
|
||||||
|
|
||||||
_onRoomStateMember(ev: MatrixEvent, state: RoomState, member: RoomMember) {
|
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => {
|
||||||
// ignore members in other rooms
|
// ignore members in other rooms
|
||||||
if (member.roomId !== this.room.roomId) {
|
if (member.roomId !== this.room.roomId) {
|
||||||
return;
|
return;
|
||||||
|
@ -89,16 +96,16 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
// blow away the users cache
|
// blow away the users cache
|
||||||
this.users = null;
|
this.users = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
async getCompletions(rawQuery: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
||||||
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||||
|
|
||||||
// lazy-load user list into matcher
|
// lazy-load user list into matcher
|
||||||
if (this.users === null) this._makeUsers();
|
if (this.users === null) this._makeUsers();
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(rawQuery, selection, force);
|
||||||
|
|
||||||
if (!command) return completions;
|
if (!command) return completions;
|
||||||
|
|
||||||
|
@ -163,7 +170,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.matcher.setObjects(this.users);
|
this.matcher.setObjects(this.users);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
|
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
|
||||||
{ completions }
|
{ completions }
|
|
@ -17,28 +17,49 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import flatMap from 'lodash/flatMap';
|
import flatMap from 'lodash/flatMap';
|
||||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||||
import {sleep} from "../../../utils/promise";
|
|
||||||
|
|
||||||
const COMPOSER_SELECTED = 0;
|
const COMPOSER_SELECTED = 0;
|
||||||
|
|
||||||
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
||||||
|
|
||||||
export default class Autocomplete extends React.Component {
|
interface IProps {
|
||||||
|
// the query string for which to show autocomplete suggestions
|
||||||
|
query: string;
|
||||||
|
// method invoked with range and text content when completion is confirmed
|
||||||
|
onConfirm: (ICompletion) => void;
|
||||||
|
// method invoked when selected (if any) completion changes
|
||||||
|
onSelectionChange?: (ICompletion, number) => void;
|
||||||
|
selection: ISelectionRange;
|
||||||
|
// The room in which we're autocompleting
|
||||||
|
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> {
|
||||||
|
autocompleter: Autocompleter;
|
||||||
|
queryRequested: string;
|
||||||
|
debounceCompletionsRequest: NodeJS.Timeout;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.autocompleter = new Autocompleter(props.room);
|
this.autocompleter = new Autocompleter(props.room);
|
||||||
this.completionPromise = null;
|
|
||||||
this.hide = this.hide.bind(this);
|
|
||||||
this.onCompletionClicked = this.onCompletionClicked.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// list of completionResults, each containing completions
|
// list of completionResults, each containing completions
|
||||||
|
@ -57,13 +78,15 @@ export default class Autocomplete extends React.Component {
|
||||||
|
|
||||||
forceComplete: false,
|
forceComplete: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.containerRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._applyNewProps();
|
this.applyNewProps();
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyNewProps(oldQuery, oldRoom) {
|
private applyNewProps(oldQuery?: string, oldRoom?: Room) {
|
||||||
if (oldRoom && this.props.room.roomId !== oldRoom.roomId) {
|
if (oldRoom && this.props.room.roomId !== oldRoom.roomId) {
|
||||||
this.autocompleter.destroy();
|
this.autocompleter.destroy();
|
||||||
this.autocompleter = new Autocompleter(this.props.room);
|
this.autocompleter = new Autocompleter(this.props.room);
|
||||||
|
@ -81,7 +104,7 @@ export default class Autocomplete extends React.Component {
|
||||||
this.autocompleter.destroy();
|
this.autocompleter.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(query, selection) {
|
complete(query: string, selection: ISelectionRange) {
|
||||||
this.queryRequested = query;
|
this.queryRequested = query;
|
||||||
if (this.debounceCompletionsRequest) {
|
if (this.debounceCompletionsRequest) {
|
||||||
clearTimeout(this.debounceCompletionsRequest);
|
clearTimeout(this.debounceCompletionsRequest);
|
||||||
|
@ -112,7 +135,7 @@ export default class Autocomplete extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
processQuery(query, selection) {
|
processQuery(query: string, selection: ISelectionRange) {
|
||||||
return this.autocompleter.getCompletions(
|
return this.autocompleter.getCompletions(
|
||||||
query, selection, this.state.forceComplete,
|
query, selection, this.state.forceComplete,
|
||||||
).then((completions) => {
|
).then((completions) => {
|
||||||
|
@ -124,7 +147,7 @@ export default class Autocomplete extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
processCompletions(completions) {
|
processCompletions(completions: IProviderCompletions[]) {
|
||||||
const completionList = flatMap(completions, (provider) => provider.completions);
|
const completionList = flatMap(completions, (provider) => provider.completions);
|
||||||
|
|
||||||
// Reset selection when completion list becomes empty.
|
// Reset selection when completion list becomes empty.
|
||||||
|
@ -159,7 +182,7 @@ export default class Autocomplete extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasSelection(): bool {
|
hasSelection(): boolean {
|
||||||
return this.countCompletions() > 0 && this.state.selectionOffset !== 0;
|
return this.countCompletions() > 0 && this.state.selectionOffset !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,7 +191,7 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// called from MessageComposerInput
|
// called from MessageComposerInput
|
||||||
moveSelection(delta): ?Completion {
|
moveSelection(delta: number) {
|
||||||
const completionCount = this.countCompletions();
|
const completionCount = this.countCompletions();
|
||||||
if (completionCount === 0) return; // there are no items to move the selection through
|
if (completionCount === 0) return; // there are no items to move the selection through
|
||||||
|
|
||||||
|
@ -177,7 +200,7 @@ export default class Autocomplete extends React.Component {
|
||||||
this.setSelection(index);
|
this.setSelection(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEscape(e): boolean {
|
onEscape(e: KeyboardEvent): boolean {
|
||||||
const completionCount = this.countCompletions();
|
const completionCount = this.countCompletions();
|
||||||
if (completionCount === 0) {
|
if (completionCount === 0) {
|
||||||
// autocomplete is already empty, so don't preventDefault
|
// autocomplete is already empty, so don't preventDefault
|
||||||
|
@ -190,9 +213,14 @@ export default class Autocomplete extends React.Component {
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide = () => {
|
||||||
this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
|
this.setState({
|
||||||
}
|
hide: true,
|
||||||
|
selectionOffset: 0,
|
||||||
|
completions: [],
|
||||||
|
completionList: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
forceComplete() {
|
forceComplete() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -207,7 +235,7 @@ export default class Autocomplete extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onCompletionClicked(selectionOffset: number): boolean {
|
onCompletionClicked = (selectionOffset: number): boolean => {
|
||||||
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
|
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -216,7 +244,7 @@ export default class Autocomplete extends React.Component {
|
||||||
this.hide();
|
this.hide();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
setSelection(selectionOffset: number) {
|
setSelection(selectionOffset: number) {
|
||||||
this.setState({selectionOffset, hide: false});
|
this.setState({selectionOffset, hide: false});
|
||||||
|
@ -225,28 +253,24 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps: IProps) {
|
||||||
this._applyNewProps(prevProps.query, prevProps.room);
|
this.applyNewProps(prevProps.query, prevProps.room);
|
||||||
// this is the selected completion, so scroll it into view if needed
|
// this is the selected completion, so scroll it into view if needed
|
||||||
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
|
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
|
||||||
if (selectedCompletion && this.container) {
|
if (selectedCompletion && this.containerRef.current) {
|
||||||
const domNode = ReactDOM.findDOMNode(selectedCompletion);
|
const domNode = ReactDOM.findDOMNode(selectedCompletion);
|
||||||
const offsetTop = domNode && domNode.offsetTop;
|
const offsetTop = domNode && domNode.offsetTop;
|
||||||
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
|
if (offsetTop > this.containerRef.current.scrollTop + this.containerRef.current.offsetHeight ||
|
||||||
offsetTop < this.container.scrollTop) {
|
offsetTop < this.containerRef.current.scrollTop) {
|
||||||
this.container.scrollTop = offsetTop - this.container.offsetTop;
|
this.containerRef.current.scrollTop = offsetTop - this.containerRef.current.offsetTop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(state, func) {
|
|
||||||
super.setState(state, func);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let position = 1;
|
let position = 1;
|
||||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
const completions = completionResult.completions.map((completion, i) => {
|
const completions = completionResult.completions.map((completion, j) => {
|
||||||
const selected = position === this.state.selectionOffset;
|
const selected = position === this.state.selectionOffset;
|
||||||
const className = classNames('mx_Autocomplete_Completion', {selected});
|
const className = classNames('mx_Autocomplete_Completion', {selected});
|
||||||
const componentPosition = position;
|
const componentPosition = position;
|
||||||
|
@ -257,7 +281,7 @@ export default class Autocomplete extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
return React.cloneElement(completion.component, {
|
return React.cloneElement(completion.component, {
|
||||||
"key": i,
|
"key": j,
|
||||||
"ref": `completion${componentPosition}`,
|
"ref": `completion${componentPosition}`,
|
||||||
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
|
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
|
||||||
className,
|
className,
|
||||||
|
@ -276,23 +300,9 @@ export default class Autocomplete extends React.Component {
|
||||||
}).filter((completion) => !!completion);
|
}).filter((completion) => !!completion);
|
||||||
|
|
||||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
<div className="mx_Autocomplete" ref={this.containerRef}>
|
||||||
{ renderedCompletions }
|
{ renderedCompletions }
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : 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),
|
|
||||||
};
|
|
|
@ -14,12 +14,28 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore - import * as EMOJIBASE actually breaks this
|
||||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||||
|
|
||||||
|
export interface IEmoji {
|
||||||
|
annotation: string;
|
||||||
|
group: number;
|
||||||
|
hexcode: string;
|
||||||
|
order: number;
|
||||||
|
shortcodes: string[];
|
||||||
|
tags: string[];
|
||||||
|
unicode: string;
|
||||||
|
emoticon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEmojiWithFilterString extends IEmoji {
|
||||||
|
filterString?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// The unicode is stored without the variant selector
|
// The unicode is stored without the variant selector
|
||||||
const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode
|
const UNICODE_TO_EMOJI = new Map<string, IEmojiWithFilterString>(); // not exported as gets for it are handled by getEmojiFromUnicode
|
||||||
export const EMOTICON_TO_EMOJI = new Map();
|
export const EMOTICON_TO_EMOJI = new Map<string, IEmojiWithFilterString>();
|
||||||
export const SHORTCODE_TO_EMOJI = new Map();
|
export const SHORTCODE_TO_EMOJI = new Map<string, IEmojiWithFilterString>();
|
||||||
|
|
||||||
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
||||||
|
|
||||||
|
@ -48,7 +64,7 @@ export const DATA_BY_CATEGORY = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store various mappings from unicode/emoticon/shortcode to the Emoji objects
|
// Store various mappings from unicode/emoticon/shortcode to the Emoji objects
|
||||||
EMOJIBASE.forEach(emoji => {
|
EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
|
||||||
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
|
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
|
||||||
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
||||||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||||
|
@ -89,3 +105,5 @@ EMOJIBASE.forEach(emoji => {
|
||||||
function stripVariation(str) {
|
function stripVariation(str) {
|
||||||
return str.replace(/[\uFE00-\uFE0F]$/, "");
|
return str.replace(/[\uFE00-\uFE0F]$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EMOJI: IEmoji[] = EMOJIBASE;
|
|
@ -2,6 +2,8 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es2016",
|
"target": "es2016",
|
||||||
|
|
Loading…
Reference in a new issue