Add a limit option for autocomplete results (#6016)
This commit is contained in:
parent
eea6ba6825
commit
bd2917aa69
11 changed files with 75 additions and 20 deletions
|
@ -93,7 +93,12 @@ export default class AutocompleteProvider {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force = false,
|
||||||
|
limit = -1,
|
||||||
|
): Promise<ICompletion[]> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,15 +82,24 @@ export default class Autocompleter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<IProviderCompletions[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force = false,
|
||||||
|
limit = -1,
|
||||||
|
): 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
|
||||||
*/
|
*/
|
||||||
// list of results from each provider, each being a list of completions or null if it times out
|
// 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 => {
|
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => {
|
||||||
return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT);
|
return await timeout(
|
||||||
|
provider.getCompletions(query, selection, force, limit),
|
||||||
|
null,
|
||||||
|
PROVIDER_COMPLETION_TIMEOUT,
|
||||||
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// map then filter to maintain the index for the map-operation, for this.providers to line up
|
// map then filter to maintain the index for the map-operation, for this.providers to line up
|
||||||
|
|
|
@ -38,7 +38,12 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force?: boolean,
|
||||||
|
limit = -1,
|
||||||
|
): Promise<ICompletion[]> {
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!command) return [];
|
if (!command) return [];
|
||||||
|
|
||||||
|
@ -55,10 +60,11 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
} else {
|
} else {
|
||||||
if (query === '/') {
|
if (query === '/') {
|
||||||
// If they have just entered `/` show everything
|
// If they have just entered `/` show everything
|
||||||
|
// We exclude the limit on purpose to have a comprehensive list
|
||||||
matches = Commands;
|
matches = Commands;
|
||||||
} else {
|
} else {
|
||||||
// otherwise fuzzy match against all of the fields
|
// otherwise fuzzy match against all of the fields
|
||||||
matches = this.matcher.match(command[1]);
|
matches = this.matcher.match(command[1], limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,12 @@ export default class CommunityProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force = false,
|
||||||
|
limit = -1,
|
||||||
|
): 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
|
||||||
|
@ -81,7 +86,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
||||||
this.matcher.setObjects(groups);
|
this.matcher.setObjects(groups);
|
||||||
|
|
||||||
const matchedString = command[0];
|
const matchedString = command[0];
|
||||||
completions = this.matcher.match(matchedString);
|
completions = this.matcher.match(matchedString, limit);
|
||||||
completions = sortBy(completions, [
|
completions = sortBy(completions, [
|
||||||
(c) => score(matchedString, c.groupId),
|
(c) => score(matchedString, c.groupId),
|
||||||
(c) => c.groupId.length,
|
(c) => c.groupId.length,
|
||||||
|
|
|
@ -36,7 +36,12 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
+ `&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: ISelectionRange, force= false): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force = false,
|
||||||
|
limit = -1,
|
||||||
|
): Promise<ICompletion[]> {
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!query || !command) {
|
if (!query || !command) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -46,7 +51,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const results = json.Results.map((result) => {
|
const maxLength = limit > -1 ? limit : json.Results.length;
|
||||||
|
const results = json.Results.slice(0, maxLength).map((result) => {
|
||||||
return {
|
return {
|
||||||
completion: result.Text,
|
completion: result.Text,
|
||||||
component: (
|
component: (
|
||||||
|
|
|
@ -84,7 +84,12 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force?: boolean,
|
||||||
|
limit = -1,
|
||||||
|
): 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
|
||||||
}
|
}
|
||||||
|
@ -93,7 +98,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
const matchedString = command[0];
|
const matchedString = command[0];
|
||||||
completions = this.matcher.match(matchedString);
|
completions = this.matcher.match(matchedString, limit);
|
||||||
|
|
||||||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||||
|
|
|
@ -33,7 +33,12 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||||
this.room = room;
|
this.room = room;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: ISelectionRange, force= false): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force = false,
|
||||||
|
limit = -1,
|
||||||
|
): Promise<ICompletion[]> {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
|
@ -87,7 +87,7 @@ export default class QueryMatcher<T extends Object> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match(query: string): T[] {
|
match(query: string, limit = -1): T[] {
|
||||||
query = this.processQuery(query);
|
query = this.processQuery(query);
|
||||||
if (this._options.shouldMatchWordsOnly) {
|
if (this._options.shouldMatchWordsOnly) {
|
||||||
query = query.replace(/[^\w]/g, '');
|
query = query.replace(/[^\w]/g, '');
|
||||||
|
@ -129,7 +129,10 @@ export default class QueryMatcher<T extends Object> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now map the keys to the result objects. Also remove any duplicates.
|
// Now map the keys to the result objects. Also remove any duplicates.
|
||||||
return uniq(matches.map((match) => match.object));
|
const dedupped = uniq(matches.map((match) => match.object));
|
||||||
|
const maxLength = limit === -1 ? dedupped.length : limit;
|
||||||
|
|
||||||
|
return dedupped.slice(0, maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private processQuery(query: string): string {
|
private processQuery(query: string): string {
|
||||||
|
|
|
@ -58,7 +58,12 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
query: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force = false,
|
||||||
|
limit = -1,
|
||||||
|
): Promise<ICompletion[]> {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
@ -90,7 +95,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
this.matcher.setObjects(matcherObjects);
|
this.matcher.setObjects(matcherObjects);
|
||||||
const matchedString = command[0];
|
const matchedString = command[0];
|
||||||
completions = this.matcher.match(matchedString);
|
completions = this.matcher.match(matchedString, limit);
|
||||||
completions = sortBy(completions, [
|
completions = sortBy(completions, [
|
||||||
(c) => score(matchedString, c.displayedAlias),
|
(c) => score(matchedString, c.displayedAlias),
|
||||||
(c) => c.displayedAlias.length,
|
(c) => c.displayedAlias.length,
|
||||||
|
|
|
@ -102,7 +102,12 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.users = null;
|
this.users = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
async getCompletions(rawQuery: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
async getCompletions(
|
||||||
|
rawQuery: string,
|
||||||
|
selection: ISelectionRange,
|
||||||
|
force = false,
|
||||||
|
limit = -1,
|
||||||
|
): 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
|
||||||
|
@ -118,7 +123,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
if (fullMatch && fullMatch !== '@') {
|
if (fullMatch && fullMatch !== '@') {
|
||||||
// Don't include the '@' in our search query - it's only used as a way to trigger completion
|
// Don't include the '@' in our search query - it's only used as a way to trigger completion
|
||||||
const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch;
|
const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch;
|
||||||
completions = this.matcher.match(query).map((user) => {
|
completions = this.matcher.match(query, limit).map((user) => {
|
||||||
const displayName = (user.name || user.userId || '');
|
const displayName = (user.name || user.userId || '');
|
||||||
return {
|
return {
|
||||||
// Length of completion should equal length of text in decorator. draft-js
|
// Length of completion should equal length of text in decorator. draft-js
|
||||||
|
|
|
@ -26,6 +26,7 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
const COMPOSER_SELECTED = 0;
|
const COMPOSER_SELECTED = 0;
|
||||||
|
const MAX_PROVIDER_MATCHES = 20;
|
||||||
|
|
||||||
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
||||||
|
|
||||||
|
@ -136,7 +137,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
processQuery(query: string, selection: ISelectionRange) {
|
processQuery(query: string, selection: ISelectionRange) {
|
||||||
return this.autocompleter.getCompletions(
|
return this.autocompleter.getCompletions(
|
||||||
query, selection, this.state.forceComplete,
|
query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
|
||||||
).then((completions) => {
|
).then((completions) => {
|
||||||
// Only ever process the completions for the most recent query being processed
|
// Only ever process the completions for the most recent query being processed
|
||||||
if (query !== this.queryRequested) {
|
if (query !== this.queryRequested) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue