From db61d343f5f9f2dc8552b97d9243c0ebfe8baa94 Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Sun, 30 Aug 2020 20:17:08 +1200 Subject: [PATCH 001/452] Add option to send/edit a message with Ctrl + Enter / Command + Enter When editing multi-line text this option helps to prevent accidentally sending a message too early. With this option, Enter just inserts a new line. For example, composing programming code in a dev chat becomes much easier when Enter just inserts a new line instead of sending the message. Signed-off-by: Clemens Zeidler --- src/components/views/rooms/EditMessageComposer.js | 8 ++++++-- src/components/views/rooms/SendMessageComposer.js | 8 ++++++-- .../settings/tabs/user/PreferencesUserSettingsTab.js | 1 + src/i18n/strings/en_EN.json | 1 + src/settings/Settings.ts | 5 +++++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 78c7de887d..636c5b27ff 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -29,9 +29,10 @@ import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import classNames from 'classnames'; import {EventStatus} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; -import {Key} from "../../../Keyboard"; +import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; +import SettingsStore from "../../../settings/SettingsStore"; function _isReply(mxEvent) { const relatesTo = mxEvent.getContent()["m.relates_to"]; @@ -135,7 +136,10 @@ export default class EditMessageComposer extends React.Component { if (event.metaKey || event.altKey || event.shiftKey) { return; } - if (event.key === Key.ENTER) { + const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); + const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event) + : event.key === Key.ENTER; + if (send) { this._sendEdit(); event.preventDefault(); } else if (event.key === Key.ESCAPE) { diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 25dcf8ccd5..dd1b67c989 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -39,11 +39,12 @@ import * as sdk from '../../../index'; import Modal from '../../../Modal'; import {_t, _td} from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; -import {Key} from "../../../Keyboard"; +import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import RateLimitedFunc from '../../../ratelimitedfunc'; import {Action} from "../../../dispatcher/actions"; +import SettingsStore from "../../../settings/SettingsStore"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -122,7 +123,10 @@ export default class SendMessageComposer extends React.Component { return; } const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; - if (event.key === Key.ENTER && !hasModifier) { + const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); + const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event) + : event.key === Key.ENTER && !hasModifier; + if (send) { this._sendMessage(); event.preventDefault(); } else if (event.key === Key.ARROW_UP) { diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index a77815a68c..64208cb8cd 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -33,6 +33,7 @@ export default class PreferencesUserSettingsTab extends React.Component { 'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.suggestEmoji', 'sendTypingNotifications', + 'MessageComposerInput.ctrlEnterToSend', ]; static TIMELINE_SETTINGS = [ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 47063bdae4..277d9c5952 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -477,6 +477,7 @@ "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", + "Use Ctrl + Enter to send a message (Mac: Command + Enter)": "Use Ctrl + Enter to send a message (Mac: Command + Enter)", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 95861e11df..d2d268b2bb 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -321,6 +321,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Show typing notifications"), default: true, }, + "MessageComposerInput.ctrlEnterToSend": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Use Ctrl + Enter to send a message (Mac: Command + Enter)"), + default: false, + }, "MessageComposerInput.autoReplaceEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Automatically replace plain text Emoji'), From 9031c58aebd08b7c6ab07e173f9895006491af5c Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Tue, 8 Sep 2020 21:46:09 +1200 Subject: [PATCH 002/452] Make settings label platform specific --- src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 277d9c5952..a66478ddc9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -477,7 +477,8 @@ "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", - "Use Ctrl + Enter to send a message (Mac: Command + Enter)": "Use Ctrl + Enter to send a message (Mac: Command + Enter)", + "Use Command + Enter to send a message": "Use Command + Enter to send a message", + "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index d2d268b2bb..afe9a50c1e 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -32,6 +32,7 @@ import UseSystemFontController from './controllers/UseSystemFontController'; import { SettingLevel } from "./SettingLevel"; import SettingController from "./controllers/SettingController"; import { RightPanelPhases } from "../stores/RightPanelStorePhases"; +import { isMac } from '../Keyboard'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -323,7 +324,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "MessageComposerInput.ctrlEnterToSend": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td("Use Ctrl + Enter to send a message (Mac: Command + Enter)"), + displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"), default: false, }, "MessageComposerInput.autoReplaceEmoji": { From 342f1d5b438710daafc0ee0f4c1a94af8dcbf9ee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 21 Sep 2020 14:36:16 -0600 Subject: [PATCH 003/452] Extremely bad support for "temporary widgets" --- src/FromWidgetPostMessageApi.js | 10 +- src/Modal.tsx | 2 +- src/WidgetMessaging.js | 30 ++++ .../views/dialogs/TempWidgetDialog.tsx | 155 ++++++++++++++++++ src/i18n/strings/en_EN.json | 4 + src/stores/TempWidgetStore.ts | 54 ++++++ src/widgets/WidgetApi.ts | 38 ++++- 7 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 src/components/views/dialogs/TempWidgetDialog.tsx create mode 100644 src/stores/TempWidgetStore.ts diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index d5d7c08d50..c5a25468a8 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -24,8 +24,10 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; -import {Capability} from "./widgets/WidgetApi"; +import {Capability, KnownWidgetActions} from "./widgets/WidgetApi"; import {objectClone} from "./utils/objects"; +import {Action} from "./dispatcher/actions"; +import {TempWidgetStore} from "./stores/TempWidgetStore"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -218,8 +220,12 @@ export default class FromWidgetPostMessageApi { if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } - } else if (action === 'get_openid') { + } else if (action === 'get_openid' + || action === KnownWidgetActions.CloseWidget) { // Handled by caller + } else if (action === KnownWidgetActions.OpenTempWidget) { + TempWidgetStore.instance.openTempWidget(event.data.data, widgetId); + this.sendResponse(event, {}); // ack } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/Modal.tsx b/src/Modal.tsx index 0a36813961..3d95bc1a2b 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; -interface IModal { +export interface IModal { elem: React.ReactNode; className?: string; beforeClosePromise?: Promise; diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index c68e926ac1..6a2eeb852c 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -147,6 +147,36 @@ export default class WidgetMessaging { }); } + sendThemeInfo(themeInfo: any) { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.UpdateThemeInfo, + data: themeInfo, + }).catch((error) => { + console.error("Failed to send theme info: ", error); + }); + } + + sendWidgetConfig(widgetConfig: any) { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.SendWidgetConfig, + data: widgetConfig, + }).catch((error) => { + console.error("Failed to send widget info: ", error); + }); + } + + sendTempCloseInfo(info: any) { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.ClosedWidgetResponse, + data: info, + }).catch((error) => { + console.error("Failed to send temp widget close info: ", error); + }); + } + start() { this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl); this.fromWidget.addListener("get_openid", this._onOpenIdRequest); diff --git a/src/components/views/dialogs/TempWidgetDialog.tsx b/src/components/views/dialogs/TempWidgetDialog.tsx new file mode 100644 index 0000000000..1fd3b26b5c --- /dev/null +++ b/src/components/views/dialogs/TempWidgetDialog.tsx @@ -0,0 +1,155 @@ +/* +Copyright 2020 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 * as React from 'react'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; +import WidgetMessaging from "../../../WidgetMessaging"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import Field from "../elements/Field"; +import { KnownWidgetActions } from "../../../widgets/WidgetApi"; +import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; + +interface IState { + messaging?: WidgetMessaging; + + androidMode: boolean; + darkTheme: boolean; + accentColor: string; +} + +interface IProps extends IDialogProps { + widgetDefinition: {url: string, data: any}; + sourceWidgetId: string; +} + +// TODO: Make a better dialog + +export default class TempWidgetDialog extends React.PureComponent { + private appFrame: React.RefObject = React.createRef(); + + constructor(props) { + super(props); + this.state = { + androidMode: false, + darkTheme: false, + accentColor: "#03b381", + }; + } + + public componentDidMount() { + // TODO: Don't violate every principle of widget creation + const messaging = new WidgetMessaging( + "TEMP_ID", + this.props.widgetDefinition.url, + this.props.widgetDefinition.url, + false, + this.appFrame.current.contentWindow, + ); + this.setState({messaging}); + } + + public componentWillUnmount() { + this.state.messaging.fromWidget.removeListener(KnownWidgetActions.CloseWidget, this.onWidgetClose); + this.state.messaging.stop(); + } + + private onLoad = () => { + this.state.messaging.getCapabilities().then(caps => { + console.log("Requested capabilities: ", caps); + this.sendTheme(); + this.state.messaging.sendWidgetConfig(this.props.widgetDefinition.data); + }); + this.state.messaging.fromWidget.addListener(KnownWidgetActions.CloseWidget, this.onWidgetClose); + }; + + private sendTheme() { + if (!this.state.messaging) return; + this.state.messaging.sendThemeInfo({ + clientName: this.state.androidMode ? "element-android" : "element-web", + isDark: this.state.darkTheme, + accentColor: this.state.accentColor, + }); + } + + public static sendExitData(sourceWidgetId: string, success: boolean, data?: any) { + const sourceMessaging = ActiveWidgetStore.getWidgetMessaging(sourceWidgetId); + if (!sourceMessaging) { + console.error("No source widget messaging for temp widget"); + return; + } + sourceMessaging.sendTempCloseInfo({success, ...data}); + } + + private onWidgetClose = (req) => { + this.props.onFinished(true); + TempWidgetDialog.sendExitData(this.props.sourceWidgetId, true, req.data); + } + + private onClientToggleChanged = (androidMode) => { + this.setState({androidMode}, () => this.sendTheme()); + }; + + private onDarkThemeChanged = (darkTheme) => { + this.setState({darkTheme}, () => this.sendTheme()); + }; + + private onAccentColorChanged = (ev) => { + this.setState({accentColor: ev.target.value}, () => this.sendTheme()); + }; + + public render() { + // TODO: Don't violate every single security principle + + const widgetUrl = this.props.widgetDefinition.url + + "?widgetId=TEMP_ID&parentUrl=" + encodeURIComponent(window.location.href); + + return +
+ + + +
+
+