parent
ce40fa1a8f
commit
b62622a814
15 changed files with 407 additions and 194 deletions
|
@ -1,10 +1,10 @@
|
|||
import Q from 'q';
|
||||
import React from 'react';
|
||||
import type {Completion, SelectionRange} from './Autocompleter';
|
||||
|
||||
export default class AutocompleteProvider {
|
||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
||||
if(commandRegex) {
|
||||
if(!commandRegex.global) {
|
||||
if (commandRegex) {
|
||||
if (!commandRegex.global) {
|
||||
throw new Error('commandRegex must have global flag set');
|
||||
}
|
||||
this.commandRegex = commandRegex;
|
||||
|
@ -14,18 +14,24 @@ export default class AutocompleteProvider {
|
|||
/**
|
||||
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
||||
*/
|
||||
getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array<string> {
|
||||
if (this.commandRegex == null) {
|
||||
getCurrentCommand(query: string, selection: {start: number, end: number}, force: boolean = false): ?string {
|
||||
let commandRegex = this.commandRegex;
|
||||
|
||||
if (force && this.shouldForceComplete()) {
|
||||
console.log('forcing complete');
|
||||
commandRegex = /[^\W]+/g;
|
||||
}
|
||||
|
||||
if (commandRegex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.commandRegex.lastIndex = 0;
|
||||
commandRegex.lastIndex = 0;
|
||||
|
||||
let match;
|
||||
while ((match = this.commandRegex.exec(query)) != null) {
|
||||
while ((match = commandRegex.exec(query)) != null) {
|
||||
let matchStart = match.index,
|
||||
matchEnd = matchStart + match[0].length;
|
||||
|
||||
if (selection.start <= matchEnd && selection.end >= matchStart) {
|
||||
return {
|
||||
command: match,
|
||||
|
@ -45,8 +51,8 @@ export default class AutocompleteProvider {
|
|||
};
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
return Q.when([]);
|
||||
async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
|
@ -57,4 +63,9 @@ export default class AutocompleteProvider {
|
|||
console.error('stub; should be implemented in subclasses');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Whether we should provide completions even if triggered forcefully, without a sigil.
|
||||
shouldForceComplete(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,63 @@
|
|||
// @flow
|
||||
|
||||
import type {Component} from 'react';
|
||||
import CommandProvider from './CommandProvider';
|
||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||
import RoomProvider from './RoomProvider';
|
||||
import UserProvider from './UserProvider';
|
||||
import EmojiProvider from './EmojiProvider';
|
||||
import Q from 'q';
|
||||
|
||||
export type SelectionRange = {
|
||||
start: number,
|
||||
end: number
|
||||
};
|
||||
|
||||
export type Completion = {
|
||||
completion: string,
|
||||
component: ?Component,
|
||||
range: SelectionRange,
|
||||
command: ?string,
|
||||
};
|
||||
|
||||
const PROVIDERS = [
|
||||
UserProvider,
|
||||
CommandProvider,
|
||||
DuckDuckGoProvider,
|
||||
RoomProvider,
|
||||
EmojiProvider,
|
||||
CommandProvider,
|
||||
DuckDuckGoProvider,
|
||||
].map(completer => completer.getInstance());
|
||||
|
||||
export function getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
return PROVIDERS.map(provider => {
|
||||
return {
|
||||
completions: provider.getCompletions(query, selection),
|
||||
provider,
|
||||
};
|
||||
});
|
||||
// Providers will get rejected if they take longer than this.
|
||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||
|
||||
export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
||||
/* Note: That this waits for all providers to return is *intentional*
|
||||
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
|
||||
|
||||
It ends up containing a list of Q promise states, which are objects with
|
||||
state (== "fulfilled" || "rejected") and value. */
|
||||
const completionsList = await Q.allSettled(
|
||||
PROVIDERS.map(provider => {
|
||||
return Q(provider.getCompletions(query, selection, force))
|
||||
.timeout(PROVIDER_COMPLETION_TIMEOUT);
|
||||
})
|
||||
);
|
||||
|
||||
return completionsList
|
||||
.filter(completion => completion.state === "fulfilled")
|
||||
.map((completionsState, i) => {
|
||||
return {
|
||||
completions: completionsState.value,
|
||||
provider: PROVIDERS[i],
|
||||
|
||||
/* the currently matched "command" the completer tried to complete
|
||||
* we pass this through so that Autocomplete can figure out when to
|
||||
* re-show itself once hidden.
|
||||
*/
|
||||
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import Fuse from 'fuse.js';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
|
@ -23,7 +22,7 @@ const COMMANDS = [
|
|||
{
|
||||
command: '/invite',
|
||||
args: '<user-id>',
|
||||
description: 'Invites user with given id to current room'
|
||||
description: 'Invites user with given id to current room',
|
||||
},
|
||||
{
|
||||
command: '/join',
|
||||
|
@ -40,6 +39,11 @@ const COMMANDS = [
|
|||
args: '<display-name>',
|
||||
description: 'Changes your display nickname',
|
||||
},
|
||||
{
|
||||
command: '/ddg',
|
||||
args: '<query>',
|
||||
description: 'Searches DuckDuckGo for results',
|
||||
}
|
||||
];
|
||||
|
||||
let COMMAND_RE = /(^\/\w*)/g;
|
||||
|
@ -54,7 +58,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
async getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
|
@ -70,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
};
|
||||
});
|
||||
}
|
||||
return Q.when(completions);
|
||||
return completions;
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import {TextualCompletion} from './Components';
|
||||
|
@ -20,61 +19,59 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
|||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
async getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (!query || !command) {
|
||||
return Q.when([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
return fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
|
||||
const response = await fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
let results = json.Results.map(result => {
|
||||
return {
|
||||
completion: result.Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={result.Text}
|
||||
description={result.Result} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
});
|
||||
if (json.Answer) {
|
||||
results.unshift({
|
||||
completion: json.Answer,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.Answer}
|
||||
description={json.AnswerType} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
|
||||
results.unshift({
|
||||
completion: json.RelatedTopics[0].Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.RelatedTopics[0].Text} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if (json.AbstractText) {
|
||||
results.unshift({
|
||||
completion: json.AbstractText,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.AbstractText} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
});
|
||||
const json = await response.json();
|
||||
let results = json.Results.map(result => {
|
||||
return {
|
||||
completion: result.Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={result.Text}
|
||||
description={result.Result} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
});
|
||||
if (json.Answer) {
|
||||
results.unshift({
|
||||
completion: json.Answer,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.Answer}
|
||||
description={json.AnswerType} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
|
||||
results.unshift({
|
||||
completion: json.RelatedTopics[0].Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.RelatedTopics[0].Text} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if (json.AbstractText) {
|
||||
results.unshift({
|
||||
completion: json.AbstractText,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.AbstractText} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
||||
import Fuse from 'fuse.js';
|
||||
import sdk from '../index';
|
||||
import {PillCompletion} from './Components';
|
||||
import type {SelectionRange, Completion} from './Autocompleter';
|
||||
|
||||
const EMOJI_REGEX = /:\w*:?/g;
|
||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||
|
@ -17,7 +17,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
this.fuse = new Fuse(EMOJI_SHORTNAMES);
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
async getCompletions(query: string, selection: SelectionRange) {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let completions = [];
|
||||
|
@ -35,7 +35,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
};
|
||||
}).slice(0, 8);
|
||||
}
|
||||
return Q.when(completions);
|
||||
return completions;
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
import Fuse from 'fuse.js';
|
||||
import {PillCompletion} from './Components';
|
||||
|
@ -21,12 +20,12 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||
|
||||
let client = MatrixClientPeg.get();
|
||||
let completions = [];
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
// the only reason we need to do this is because Fuse only matches on properties
|
||||
this.fuse.set(client.getRooms().filter(room => !!room).map(room => {
|
||||
|
@ -48,7 +47,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
};
|
||||
}).slice(0, 4);
|
||||
}
|
||||
return Q.when(completions);
|
||||
return completions;
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
@ -68,4 +67,8 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
{completions}
|
||||
</div>;
|
||||
}
|
||||
|
||||
shouldForceComplete(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,11 +20,11 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
this.fuse.set(this.users);
|
||||
completions = this.fuse.search(command[0]).map(user => {
|
||||
|
@ -37,11 +37,11 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
title={displayName}
|
||||
description={user.userId} />
|
||||
),
|
||||
range
|
||||
range,
|
||||
};
|
||||
}).slice(0, 4);
|
||||
}
|
||||
return Q.when(completions);
|
||||
return completions;
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
@ -64,4 +64,8 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
{completions}
|
||||
</div>;
|
||||
}
|
||||
|
||||
shouldForceComplete(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue