From 256c468c15a3f2af8e7c52098dee5813173d8500 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 18 Nov 2021 12:47:11 +0000 Subject: [PATCH] Chat Effects & Commands in thread context (#7138) --- src/SlashCommands.tsx | 32 +++++++++++++++++-- src/autocomplete/AutocompleteProvider.tsx | 14 +++++++- src/autocomplete/Autocompleter.ts | 5 +-- src/autocomplete/CommandProvider.tsx | 14 +++++--- src/autocomplete/CommunityProvider.tsx | 6 ++-- src/autocomplete/EmojiProvider.tsx | 6 ++-- src/autocomplete/NotifProvider.tsx | 8 ++--- src/autocomplete/QueryMatcher.ts | 2 ++ src/autocomplete/RoomProvider.tsx | 5 +-- src/autocomplete/UserProvider.tsx | 9 ++++-- src/components/structures/RoomView.tsx | 6 +++- src/components/views/rooms/Autocomplete.tsx | 6 ++-- .../views/rooms/SendMessageComposer.tsx | 7 +++- src/effects/utils.ts | 5 ++- src/stores/widgets/StopGapWidgetDriver.ts | 14 +++++--- 15 files changed, 108 insertions(+), 31 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index c8884cead4..f172e6ecc6 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -57,6 +57,7 @@ import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpD import { logger } from "matrix-js-sdk/src/logger"; import { shouldShowComponent } from "./customisations/helpers/UIComponents"; +import { TimelineRenderingType } from './contexts/RoomContext'; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -102,6 +103,7 @@ interface ICommandOpts { category: string; hideCompletionAfterSpace?: boolean; isEnabled?(): boolean; + renderingTypes?: TimelineRenderingType[]; } export class Command { @@ -112,7 +114,8 @@ export class Command { runFn: undefined | RunFn; category: string; hideCompletionAfterSpace: boolean; - _isEnabled?: () => boolean; + private _isEnabled?: () => boolean; + public renderingTypes?: TimelineRenderingType[]; constructor(opts: ICommandOpts) { this.command = opts.command; @@ -123,6 +126,7 @@ export class Command { this.category = opts.category || CommandCategories.other; this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false; this._isEnabled = opts.isEnabled; + this.renderingTypes = opts.renderingTypes; } getCommand() { @@ -143,7 +147,7 @@ export class Command { return _t('Usage') + ': ' + this.getCommandWithArgs(); } - isEnabled() { + isEnabled(): boolean { return this._isEnabled ? this._isEnabled() : true; } } @@ -271,6 +275,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'nick', @@ -283,6 +288,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'myroomnick', @@ -302,6 +308,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'roomavatar', @@ -319,6 +326,7 @@ export const Commands = [ })); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'myroomavatar', @@ -345,6 +353,7 @@ export const Commands = [ })); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'myavatar', @@ -362,6 +371,7 @@ export const Commands = [ })); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'topic', @@ -387,6 +397,7 @@ export const Commands = [ return success(); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'roomname', @@ -399,6 +410,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'invite', @@ -462,6 +474,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'join', @@ -577,6 +590,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'part', @@ -620,6 +634,7 @@ export const Commands = [ return success(leaveRoomBehaviour(targetRoomId)); }, category: CommandCategories.actions, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'kick', @@ -635,6 +650,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'ban', @@ -650,6 +666,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'unban', @@ -666,6 +683,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'ignore', @@ -755,6 +773,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'deop', @@ -776,6 +795,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'devtools', @@ -838,6 +858,7 @@ export const Commands = [ } }, category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'verify', @@ -903,6 +924,7 @@ export const Commands = [ return reject(this.getUsage()); }, category: CommandCategories.advanced, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: 'discardsession', @@ -916,6 +938,7 @@ export const Commands = [ return success(); }, category: CommandCategories.advanced, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "rainbow", @@ -1053,6 +1076,7 @@ export const Commands = [ call.setRemoteOnHold(true); return success(); }, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "unholdcall", @@ -1066,6 +1090,7 @@ export const Commands = [ call.setRemoteOnHold(false); return success(); }, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "converttodm", @@ -1075,6 +1100,7 @@ export const Commands = [ const room = MatrixClientPeg.get().getRoom(roomId); return success(guessAndSetDMRoom(room, true)); }, + renderingTypes: [TimelineRenderingType.Room], }), new Command({ command: "converttoroom", @@ -1084,6 +1110,7 @@ export const Commands = [ const room = MatrixClientPeg.get().getRoom(roomId); return success(guessAndSetDMRoom(room, false)); }, + renderingTypes: [TimelineRenderingType.Room], }), // Command definitions for autocompletion ONLY: @@ -1117,6 +1144,7 @@ export const Commands = [ })()); }, category: CommandCategories.effects, + renderingTypes: [TimelineRenderingType.Room], }); }), ]; diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 2d82a9f591..14ade84bf3 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import { TimelineRenderingType } from '../contexts/RoomContext'; import type { ICompletion, ISelectionRange } from './Autocompleter'; export interface ICommand { @@ -27,11 +28,19 @@ export interface ICommand { }; } +export interface IAutocompleteOptions { + commandRegex?: RegExp; + forcedCommandRegex?: RegExp; + renderingType?: TimelineRenderingType; +} + export default abstract class AutocompleteProvider { commandRegex: RegExp; forcedCommandRegex: RegExp; - protected constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { + protected renderingType: TimelineRenderingType = TimelineRenderingType.Room; + + protected constructor({ commandRegex, forcedCommandRegex, renderingType }: IAutocompleteOptions) { if (commandRegex) { if (!commandRegex.global) { throw new Error('commandRegex must have global flag set'); @@ -44,6 +53,9 @@ export default abstract class AutocompleteProvider { } this.forcedCommandRegex = forcedCommandRegex; } + if (renderingType) { + this.renderingType = renderingType; + } } destroy() { diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 555429e75f..aeee335e04 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -28,6 +28,7 @@ import { timeout } from "../utils/promise"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; import SpaceProvider from "./SpaceProvider"; import SpaceStore from "../stores/spaces/SpaceStore"; +import { TimelineRenderingType } from '../contexts/RoomContext'; export interface ISelectionRange { beginning?: boolean; // whether the selection is in the first block of the editor or not @@ -75,10 +76,10 @@ export default class Autocompleter { room: Room; providers: AutocompleteProvider[]; - constructor(room: Room) { + constructor(room: Room, renderingType: TimelineRenderingType = TimelineRenderingType.Room) { this.room = room; this.providers = PROVIDERS.map((Prov) => { - return new Prov(room); + return new Prov(room, renderingType); }); } diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index 143b7e4cdc..13daadbcf6 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -24,17 +24,20 @@ import QueryMatcher from './QueryMatcher'; import { TextualCompletion } from './Components'; import { ICompletion, ISelectionRange } from "./Autocompleter"; import { Command, Commands, CommandMap } from '../SlashCommands'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const COMMAND_RE = /(^\/\w*)(?: .*)?/g; export default class CommandProvider extends AutocompleteProvider { matcher: QueryMatcher; - constructor() { - super(COMMAND_RE); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: COMMAND_RE, renderingType }); this.matcher = new QueryMatcher(Commands, { keys: ['command', 'args', 'description'], funcs: [({ aliases }) => aliases.join(" ")], // aliases + context: renderingType, }); } @@ -47,7 +50,7 @@ export default class CommandProvider extends AutocompleteProvider { const { command, range } = this.getCurrentCommand(query, selection); if (!command) return []; - let matches = []; + let matches: Command[] = []; // check if the full match differs from the first word (i.e. returns false if the command has args) if (command[0] !== command[1]) { // The input looks like a command with arguments, perform exact match @@ -68,7 +71,10 @@ export default class CommandProvider extends AutocompleteProvider { } } - return matches.filter(cmd => cmd.isEnabled()).map((result) => { + return matches.filter(cmd => { + const display = !cmd.renderingTypes || cmd.renderingTypes.includes(this.renderingType); + return cmd.isEnabled() && display; + }).map((result) => { let completion = result.getCommand() + ' '; const usedAlias = result.aliases.find(alias => `/${alias}` === command[1]); // If the command (or an alias) is the same as the one they entered, we don't want to discard their arguments diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index 4b42f4c64e..3e967063ab 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -28,6 +28,8 @@ import { ICompletion, ISelectionRange } from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; import { mediaFromMxc } from "../customisations/Media"; import BaseAvatar from '../components/views/avatars/BaseAvatar'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const COMMUNITY_REGEX = /\B\+\S*/g; @@ -43,8 +45,8 @@ function score(query, space) { export default class CommunityProvider extends AutocompleteProvider { matcher: QueryMatcher; - constructor() { - super(COMMUNITY_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: COMMUNITY_REGEX, renderingType }); this.matcher = new QueryMatcher([], { keys: ['groupId', 'name', 'shortDescription'], }); diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 326651e037..c43eba0bf1 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -28,6 +28,8 @@ import SettingsStore from "../settings/SettingsStore"; import { EMOJI, IEmoji } from '../emoji'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const LIMIT = 20; @@ -64,8 +66,8 @@ export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; - constructor() { - super(EMOJI_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: EMOJI_REGEX, renderingType }); this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index aa4f1174dc..52e235a1b1 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -23,15 +23,13 @@ import { MatrixClientPeg } from '../MatrixClientPeg'; import { PillCompletion } from './Components'; import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const AT_ROOM_REGEX = /@\S*/g; export default class NotifProvider extends AutocompleteProvider { - room: Room; - - constructor(room) { - super(AT_ROOM_REGEX); - this.room = room; + constructor(public room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: AT_ROOM_REGEX, renderingType }); } async getCompletions( diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 3948be301c..bdfd94790b 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -18,6 +18,7 @@ limitations under the License. import { at, uniq } from 'lodash'; import { removeHiddenChars } from "matrix-js-sdk/src/utils"; +import { TimelineRenderingType } from '../contexts/RoomContext'; interface IOptions { keys: Array; @@ -25,6 +26,7 @@ interface IOptions { shouldMatchWordsOnly?: boolean; // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true fuzzy?: boolean; + context?: TimelineRenderingType; } /** diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index ced0e7ad17..1879c85fe3 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -29,6 +29,7 @@ import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; import SpaceStore from "../stores/spaces/SpaceStore"; +import { TimelineRenderingType } from "../contexts/RoomContext"; const ROOM_REGEX = /\B#\S*/g; @@ -48,8 +49,8 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") { export default class RoomProvider extends AutocompleteProvider { protected matcher: QueryMatcher; - constructor() { - super(ROOM_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ commandRegex: ROOM_REGEX, renderingType }); this.matcher = new QueryMatcher([], { keys: ['displayedAlias', 'matchName'], }); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 48854657de..e4b285df84 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -33,6 +33,7 @@ import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { makeUserPermalink } from "../utils/permalinks/Permalinks"; import { ICompletion, ISelectionRange } from "./Autocompleter"; import MemberAvatar from '../components/views/avatars/MemberAvatar'; +import { TimelineRenderingType } from '../contexts/RoomContext'; const USER_REGEX = /\B@\S*/g; @@ -50,8 +51,12 @@ export default class UserProvider extends AutocompleteProvider { users: RoomMember[]; room: Room; - constructor(room: Room) { - super(USER_REGEX, FORCED_USER_REGEX); + constructor(room: Room, renderingType?: TimelineRenderingType) { + super({ + commandRegex: USER_REGEX, + forcedCommandRegex: FORCED_USER_REGEX, + renderingType, + }); this.room = room; this.matcher = new QueryMatcher([], { keys: ['name'], diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 833d3d3bcc..98d55f8ae6 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -957,7 +957,11 @@ export class RoomView extends React.Component { CHAT_EFFECTS.forEach(effect => { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { - dis.dispatch({ action: `effects.${effect.command}` }); + // For initial threads launch, chat effects are disabled + // see #19731 + if (!SettingsStore.getValue("feature_thread") || !ev.isThreadRelation) { + dis.dispatch({ action: `effects.${effect.command}` }); + } } }); }; diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 34909baef1..0991a230ab 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -24,6 +24,7 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomContext from '../../../contexts/RoomContext'; const MAX_PROVIDER_MATCHES = 20; @@ -57,11 +58,11 @@ export default class Autocomplete extends React.PureComponent { debounceCompletionsRequest: number; private containerRef = createRef(); + public static contextType = RoomContext; + constructor(props) { super(props); - this.autocompleter = new Autocompleter(props.room); - this.state = { // list of completionResults, each containing completions completions: [], @@ -82,6 +83,7 @@ export default class Autocomplete extends React.PureComponent { } componentDidMount() { + this.autocompleter = new Autocompleter(this.props.room, this.context.timelineRenderingType); this.applyNewProps(); } diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index ba8f99f47f..3b1db6bb90 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -492,7 +492,12 @@ export class SendMessageComposer extends React.Component { if (containsEmoji(content, effect.emojis)) { - dis.dispatch({ action: `effects.${effect.command}` }); + // For initial threads launch, chat effects are disabled + // see #19731 + const isNotThread = this.props.relation?.rel_type !== RelationType.Thread; + if (!SettingsStore.getValue("feature_thread") || !isNotThread) { + dis.dispatch({ action: `effects.${effect.command}` }); + } } }); if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { diff --git a/src/effects/utils.ts b/src/effects/utils.ts index 9f6d7a512e..559c8e04bb 100644 --- a/src/effects/utils.ts +++ b/src/effects/utils.ts @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ + +import { IContent } from "matrix-js-sdk/src/models/event"; + /** * Checks a message if it contains one of the provided emojis * @param {Object} content The message * @param {Array} emojis The list of emojis to check for */ -export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array): boolean => { +export const containsEmoji = (content: IContent, emojis: Array): boolean => { return emojis.some((emoji) => content.body && content.body.includes(emoji)); }; diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index da844b1923..d52d7d9a8e 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -38,15 +38,16 @@ import WidgetCapabilitiesPromptDialog from "../../components/views/dialogs/Widge import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions"; import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore"; import { WidgetType } from "../../widgets/WidgetType"; -import { EventType } from "matrix-js-sdk/src/@types/event"; +import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import { CHAT_EFFECTS } from "../../effects"; import { containsEmoji } from "../../effects/utils"; import dis from "../../dispatcher/dispatcher"; import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks"; -import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { IContent, IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { logger } from "matrix-js-sdk/src/logger"; +import SettingsStore from "../../settings/SettingsStore"; // TODO: Purge this from the universe @@ -141,7 +142,7 @@ export class StopGapWidgetDriver extends WidgetDriver { public async sendEvent( eventType: string, - content: any, + content: IContent, stateKey: string = null, targetRoomId: string = null, ): Promise { @@ -164,7 +165,12 @@ export class StopGapWidgetDriver extends WidgetDriver { if (eventType === EventType.RoomMessage) { CHAT_EFFECTS.forEach((effect) => { if (containsEmoji(content, effect.emojis)) { - dis.dispatch({ action: `effects.${effect.command}` }); + // For initial threads launch, chat effects are disabled + // see #19731 + const isNotThread = content["m.relates_to"].rel_type !== RelationType.Thread; + if (!SettingsStore.getValue("feature_thread") || !isNotThread) { + dis.dispatch({ action: `effects.${effect.command}` }); + } } }); }