Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/a11y/composer-list-autocomplete
Conflicts: src/autocomplete/AutocompleteProvider.tsx src/components/views/rooms/Autocomplete.tsx
This commit is contained in:
@@ -93,7 +93,14 @@ export default abstract class AutocompleteProvider {
|
||||
};
|
||||
}
|
||||
|
||||
abstract getCompletions(query: string, selection: ISelectionRange, force: boolean): Promise<ICompletion[]>;
|
||||
abstract getCompletions(
|
||||
query: string,
|
||||
selection: ISelectionRange,
|
||||
force: boolean,
|
||||
limit: number,
|
||||
): Promise<ICompletion[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
abstract getName(): string;
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import EmojiProvider from './EmojiProvider';
|
||||
import NotifProvider from './NotifProvider';
|
||||
import {timeout} from "../utils/promise";
|
||||
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import SpaceProvider from "./SpaceProvider";
|
||||
|
||||
export interface ISelectionRange {
|
||||
beginning?: boolean; // whether the selection is in the first block of the editor or not
|
||||
@@ -56,6 +58,11 @@ const PROVIDERS = [
|
||||
DuckDuckGoProvider,
|
||||
];
|
||||
|
||||
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
PROVIDERS.push(SpaceProvider);
|
||||
}
|
||||
|
||||
// Providers will get rejected if they take longer than this.
|
||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||
|
||||
@@ -82,15 +89,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,
|
||||
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
|
||||
*/
|
||||
// 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);
|
||||
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => {
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
if (!command) return [];
|
||||
|
||||
@@ -55,10 +60,11 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||
} else {
|
||||
if (query === '/') {
|
||||
// If they have just entered `/` show everything
|
||||
// We exclude the limit on purpose to have a comprehensive list
|
||||
matches = Commands;
|
||||
} else {
|
||||
// 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');
|
||||
|
||||
// Disable autocompletions when composing commands because of various issues
|
||||
@@ -81,7 +86,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
||||
this.matcher.setObjects(groups);
|
||||
|
||||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString);
|
||||
completions = this.matcher.match(matchedString, limit);
|
||||
completions = sortBy(completions, [
|
||||
(c) => score(matchedString, c.groupId),
|
||||
(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)}`;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!query || !command) {
|
||||
return [];
|
||||
@@ -46,7 +51,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||
method: 'GET',
|
||||
});
|
||||
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 {
|
||||
completion: result.Text,
|
||||
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")) {
|
||||
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);
|
||||
if (command) {
|
||||
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'
|
||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||
|
||||
@@ -33,7 +33,12 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||
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 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);
|
||||
if (this._options.shouldMatchWordsOnly) {
|
||||
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.
|
||||
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 {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2017, 2018, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,17 +16,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import {uniqBy, sortBy} from "lodash";
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import * as sdk from '../index';
|
||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
import {uniqBy, sortBy} from "lodash";
|
||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
const ROOM_REGEX = /\B#\S*/g;
|
||||
|
||||
@@ -49,7 +50,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") {
|
||||
}
|
||||
|
||||
export default class RoomProvider extends AutocompleteProvider {
|
||||
matcher: QueryMatcher<Room>;
|
||||
protected matcher: QueryMatcher<Room>;
|
||||
|
||||
constructor() {
|
||||
super(ROOM_REGEX);
|
||||
@@ -58,15 +59,28 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
|
||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||
protected getRooms() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
let rooms = cli.getVisibleRooms();
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
rooms = rooms.filter(r => !r.isSpaceRoom());
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
async getCompletions(
|
||||
query: string,
|
||||
selection: ISelectionRange,
|
||||
force = false,
|
||||
limit = -1,
|
||||
): Promise<ICompletion[]> {
|
||||
let completions = [];
|
||||
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
|
||||
let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => {
|
||||
let matcherObjects = this.getRooms().reduce((aliases, room) => {
|
||||
if (room.getCanonicalAlias()) {
|
||||
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
|
||||
}
|
||||
@@ -90,7 +104,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
|
||||
this.matcher.setObjects(matcherObjects);
|
||||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString);
|
||||
completions = this.matcher.match(matchedString, limit);
|
||||
completions = sortBy(completions, [
|
||||
(c) => score(matchedString, c.displayedAlias),
|
||||
(c) => c.displayedAlias.length,
|
||||
@@ -110,7 +124,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4);
|
||||
}).filter((completion) => !!completion.completion && completion.completion.length > 0);
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import RoomProvider from "./RoomProvider";
|
||||
|
||||
export default class SpaceProvider extends RoomProvider {
|
||||
protected getRooms() {
|
||||
return MatrixClientPeg.get().getVisibleRooms().filter(r => r.isSpaceRoom());
|
||||
}
|
||||
|
||||
getName() {
|
||||
return _t("Spaces");
|
||||
}
|
||||
|
||||
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||
return (
|
||||
<div
|
||||
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||
role="listbox"
|
||||
aria-label={_t("Space Autocomplete")}
|
||||
>
|
||||
{ completions }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,12 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
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');
|
||||
|
||||
// lazy-load user list into matcher
|
||||
@@ -118,7 +123,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
if (fullMatch && fullMatch !== '@') {
|
||||
// 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;
|
||||
completions = this.matcher.match(query).map((user) => {
|
||||
completions = this.matcher.match(query, limit).map((user) => {
|
||||
const displayName = (user.name || user.userId || '');
|
||||
return {
|
||||
// Length of completion should equal length of text in decorator. draft-js
|
||||
|
||||
Reference in New Issue
Block a user