From b4af0140d425c03ffe2e8044edf30f1fe8326d7a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 12:38:47 -0600 Subject: [PATCH 01/17] Render Jitsi widget state events in a more obvious way A clear improvement to this would be to include join/leave buttons in the tiles, however this is currently deferred. --- res/css/_components.scss | 1 + .../views/messages/_MJitsiWidgetEvent.scss | 55 ++++++++++++++ src/TextForEvent.js | 4 - .../views/messages/MJitsiWidgetEvent.tsx | 74 +++++++++++++++++++ src/components/views/rooms/EventTile.js | 20 ++++- src/i18n/strings/en_EN.json | 4 + 6 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 res/css/views/messages/_MJitsiWidgetEvent.scss create mode 100644 src/components/views/messages/MJitsiWidgetEvent.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 54e7436886..26ad802955 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -139,6 +139,7 @@ @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MJitsiWidgetEvent.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; diff --git a/res/css/views/messages/_MJitsiWidgetEvent.scss b/res/css/views/messages/_MJitsiWidgetEvent.scss new file mode 100644 index 0000000000..3e51e89744 --- /dev/null +++ b/res/css/views/messages/_MJitsiWidgetEvent.scss @@ -0,0 +1,55 @@ +/* +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. +*/ + +.mx_MJitsiWidgetEvent { + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content; + + &::before { + grid-column: 1; + grid-row: 1 / 3; + width: 16px; + height: 16px; + content: ""; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + background-color: $composer-e2e-icon-color; // XXX: Variable abuse + margin-top: 4px; + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + + .mx_MJitsiWidgetEvent_title { + font-weight: 600; + font-size: $font-15px; + grid-column: 2; + grid-row: 1; + } + + .mx_MJitsiWidgetEvent_subtitle { + grid-column: 2; + grid-row: 2; + } + + .mx_MJitsiWidgetEvent_title, + .mx_MJitsiWidgetEvent_subtitle { + overflow-wrap: break-word; + } +} diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a76c1f59e6..46e1878d5f 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -476,10 +476,6 @@ function textForWidgetEvent(event) { const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); const {name, type, url} = event.getContent() || {}; - if (WidgetType.JITSI.matches(type) || WidgetType.JITSI.matches(prevType)) { - return textForJitsiWidgetEvent(event, senderName, url, prevUrl); - } - let widgetName = name || prevName || type || prevType || ''; // Apply sentence case to widget name if (widgetName && widgetName.length > 0) { diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx new file mode 100644 index 0000000000..1bfefbff6a --- /dev/null +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -0,0 +1,74 @@ +/* +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 React from 'react'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { _t } from "../../../languageHandler"; + +interface IProps { + mxEvent: MatrixEvent; +} + +interface IState { +} + +export default class MJitsiWidgetEvent extends React.PureComponent { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const url = this.props.mxEvent.getContent()['url']; + const prevUrl = this.props.mxEvent.getPrevContent()['url']; + const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender(); + + if (!url) { + // removed + return ( +
+
+ {_t("Video conference ended by %(senderName)s", {senderName})} +
+
+ ); + } else if (prevUrl) { + // modified + return ( +
+
+ {_t("Video conference updated by %(senderName)s", {senderName})} +
+
+ {_t("Join the conference at the top of this room.")} +
+
+ ); + } else { + // assume added + return ( +
+
+ {_t("Video conference started by %(senderName)s", {senderName})} +
+
+ {_t("Join the conference at the top of this room.")} +
+
+ ); + } + } +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index ab9f240f2d..ef9317704d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -34,6 +34,7 @@ import * as ObjectUtils from "../../../ObjectUtils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {E2E_STATE} from "./E2EIcon"; import {toRem} from "../../../utils/units"; +import {WidgetType} from "../../../widgets/WidgetType"; const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', @@ -110,6 +111,19 @@ export function getHandlerTile(ev) { } } + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) + if (type === "im.vector.modular.widgets") { + let type = ev.getContent()['type']; + if (!type) { + // deleted/invalid widget - try the past widget type + type = ev.getPrevContent()['type']; + } + + if (WidgetType.JITSI.matches(type)) { + return "messages.MJitsiWidgetEvent"; + } + } + return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; } @@ -619,16 +633,18 @@ export default class EventTile extends React.Component { const msgtype = content.msgtype; const eventType = this.props.mxEvent.getType(); + let tileHandler = getHandlerTile(this.props.mxEvent); + // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || (eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) || - (eventType === "m.room.encryption"); + (eventType === "m.room.encryption") || + (tileHandler === "messages.MJitsiWidgetEvent"); let isInfoMessage = ( !isBubbleMessage && eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType !== 'm.room.create' ); - let tileHandler = getHandlerTile(this.props.mxEvent); // If we're showing hidden events in the timeline, we should use the // source tile when there's no regular tile for an event and also for // replace relations (which otherwise would display as a confusing diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b2b4e01202..9d1d39477c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1405,6 +1405,10 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "Video conference ended by %(senderName)s": "Video conference ended by %(senderName)s", + "Video conference updated by %(senderName)s": "Video conference updated by %(senderName)s", + "Join the conference at the top of this room.": "Join the conference at the top of this room.", + "Video conference started by %(senderName)s": "Video conference started by %(senderName)s", "You have ignored this user, so their message is hidden. Show anyways.": "You have ignored this user, so their message is hidden. Show anyways.", "You verified %(name)s": "You verified %(name)s", "You cancelled verifying %(name)s": "You cancelled verifying %(name)s", From 12fb1ee1cf82a2d4c70636681314c5bc1a087a78 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 12:43:28 -0600 Subject: [PATCH 02/17] Clean up now-unused code --- src/TextForEvent.js | 19 ------------------- src/i18n/strings/en_EN.json | 3 --- 2 files changed, 22 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 46e1878d5f..c55380bd9b 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -19,7 +19,6 @@ import { _t } from './languageHandler'; import * as Roles from './Roles'; import {isValid3pidInvite} from "./RoomInvite"; import SettingsStore from "./settings/SettingsStore"; -import {WidgetType} from "./widgets/WidgetType"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; function textForMemberEvent(ev) { @@ -501,24 +500,6 @@ function textForWidgetEvent(event) { } } -function textForJitsiWidgetEvent(event, senderName, url, prevUrl) { - if (url) { - if (prevUrl) { - return _t('Group call modified by %(senderName)s', { - senderName, - }); - } else { - return _t('Group call started by %(senderName)s', { - senderName, - }); - } - } else { - return _t('Group call ended by %(senderName)s', { - senderName, - }); - } -} - function textForMjolnirEvent(event) { const senderName = event.getSender(); const {entity: prevEntity} = event.getPrevContent(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9d1d39477c..01d334505c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -280,9 +280,6 @@ "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", - "Group call modified by %(senderName)s": "Group call modified by %(senderName)s", - "Group call started by %(senderName)s": "Group call started by %(senderName)s", - "Group call ended by %(senderName)s": "Group call ended by %(senderName)s", "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s removed the rule banning users matching %(glob)s", "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s removed the rule banning rooms matching %(glob)s", "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s removed the rule banning servers matching %(glob)s", From 1ffc6d5bd34fa2d2e87c0ea533c7cd2d9104cf5f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 14:35:50 -0600 Subject: [PATCH 03/17] Make the hangup button do things for conference calls Behaviour constraints: * If you're not in the conference, use a grey button that does nothing. * If you're in the conference, show a button: * If you're able to modify widgets in the room, annotate it in the context of ending the call for everyone and remove the widget. Use a confirmation dialog. * If you're not able to modify widgets in the room, hang up. For this we know that persistent Jitsi widgets will mean that the user is in the call, so we use that to determine if they are actually participating. --- res/css/views/rooms/_MessageComposer.scss | 2 +- src/CallHandler.js | 77 ++++++++++++------- src/WidgetMessaging.js | 11 +++ src/components/views/rooms/MessageComposer.js | 63 +++++++++++++-- src/i18n/strings/en_EN.json | 7 +- src/stores/WidgetStore.ts | 19 +++++ src/widgets/WidgetApi.ts | 7 +- 7 files changed, 144 insertions(+), 42 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index a403a8dc4c..71c0db947e 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -217,7 +217,7 @@ limitations under the License. } } - &.mx_MessageComposer_hangup::before { + &.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before { background-color: $warning-color; } } diff --git a/src/CallHandler.js b/src/CallHandler.js index ad40332af5..e40c97f025 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -70,6 +70,8 @@ import {base32} from "rfc4648"; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; +import WidgetStore from "./stores/WidgetStore"; +import ActiveWidgetStore from "./stores/ActiveWidgetStore"; global.mxCalls = { //room_id: MatrixCall @@ -310,6 +312,14 @@ function _onAction(payload) { console.info("Place conference call in %s", payload.room_id); _startCallApp(payload.room_id, payload.type); break; + case 'end_conference': + console.info("Terminating conference call in %s", payload.room_id); + _terminateCallApp(payload.room_id); + break; + case 'hangup_conference': + console.info("Leaving conference call in %s", payload.room_id); + _hangupWithCallApp(payload.room_id); + break; case 'incoming_call': { if (callHandler.getAnyActiveCall()) { @@ -357,10 +367,12 @@ async function _startCallApp(roomId, type) { show: true, }); + // prevent double clicking the call button const room = MatrixClientPeg.get().getRoom(roomId); const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); - - if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) { + const hasJitsi = currentJitsiWidgets.length > 0 + || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI); + if (hasJitsi) { Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { title: _t('Call in Progress'), description: _t('A call is currently being placed!'), @@ -368,33 +380,6 @@ async function _startCallApp(roomId, type) { return; } - if (currentJitsiWidgets.length > 0) { - console.warn( - "Refusing to start conference call widget in " + roomId + - " a conference call widget is already present", - ); - - if (WidgetUtils.canUserModifyWidgets(roomId)) { - Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, { - title: _t('End Call'), - description: _t('Remove the group call from the room?'), - button: _t('End Call'), - cancelButton: _t('Cancel'), - onFinished: (endCall) => { - if (endCall) { - WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']); - } - }, - }); - } else { - Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, { - title: _t('Call in Progress'), - description: _t("You don't have permission to remove the call from the room"), - }); - } - return; - } - const jitsiDomain = Jitsi.getInstance().preferredDomain; const jitsiAuth = await Jitsi.getInstance().getJitsiAuth(); let confId; @@ -444,6 +429,40 @@ async function _startCallApp(roomId, type) { }); } +function _terminateCallApp(roomId) { + Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, { + hasCancelButton: true, + title: _t("End conference"), + description: _t("Ending the conference will end the call for everyone. Continue?"), + button: _t("End conference"), + onFinished: (proceed) => { + if (!proceed) return; + + // We'll just obliterate them all. There should only ever be one, but might as well + // be safe. + const roomInfo = WidgetStore.instance.getRoom(roomId); + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + // setting invalid content removes it + WidgetUtils.setRoomWidget(roomId, w.id); + }); + }, + }); +} + +function _hangupWithCallApp(roomId) { + const roomInfo = WidgetStore.instance.getRoom(roomId); + if (!roomInfo) return; // "should never happen" clauses go here + + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + const messaging = ActiveWidgetStore.getWidgetMessaging(w.id); + if (!messaging) return; // more "should never happen" words + + messaging.hangup(); + }); +} + // FIXME: Nasty way of making sure we only register // with the dispatcher once if (!global.mxCallHandler) { diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index c68e926ac1..0f8626ec66 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -107,6 +107,17 @@ export default class WidgetMessaging { }); } + /** + * Tells the widget to hang up on its call. + * @returns {Promise<*>} Resolves when teh widget has acknowledged the message. + */ + hangup() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.Hangup, + }); + } + /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 81c2ae7a33..3eab58557e 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 2018 New Vector Ltd +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. @@ -32,6 +33,10 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; import {UIFeature} from "../../../settings/UIFeature"; +import WidgetStore from "../../../stores/WidgetStore"; +import WidgetUtils from "../../../utils/WidgetUtils"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -85,8 +90,15 @@ VideoCallButton.propTypes = { }; function HangupButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const onHangupClick = () => { + if (props.isConference) { + dis.dispatch({ + action: props.canEndConference ? 'end_conference' : 'hangup_conference', + room_id: props.roomId, + }); + return; + } + const call = CallHandler.getCallForRoom(props.roomId); if (!call) { return; @@ -98,14 +110,28 @@ function HangupButton(props) { room_id: call.roomId, }); }; - return (); + title={tooltip} + disabled={!canLeaveConference} + /> + ); } HangupButton.propTypes = { roomId: PropTypes.string.isRequired, + isConference: PropTypes.bool.isRequired, + canEndConference: PropTypes.bool, + isInConference: PropTypes.bool, }; const EmojiButton = ({addEmoji}) => { @@ -226,12 +252,17 @@ export default class MessageComposer extends React.Component { this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); this._onTombstoneClick = this._onTombstoneClick.bind(this); this.renderPlaceholderText = this.renderPlaceholderText.bind(this); + WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate); + ActiveWidgetStore.on('update', this._onActiveWidgetUpdate); this._dispatcherRef = null; + this.state = { isQuoting: Boolean(RoomViewStore.getQuotingEvent()), tombstone: this._getRoomTombstone(), canSendMessages: this.props.room.maySendMessage(), showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"), + hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room), + joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room), }; } @@ -247,6 +278,14 @@ export default class MessageComposer extends React.Component { } }; + _onWidgetUpdate = () => { + this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)}); + }; + + _onActiveWidgetUpdate = () => { + this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)}); + }; + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); @@ -277,6 +316,8 @@ export default class MessageComposer extends React.Component { if (this._roomStoreToken) { this._roomStoreToken.remove(); } + WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate); + ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate); dis.unregister(this.dispatcherRef); } @@ -392,9 +433,19 @@ export default class MessageComposer extends React.Component { } if (this.state.showCallButtons) { - if (callInProgress) { + if (this.state.hasConference) { + const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId); controls.push( - , + , + ); + } else if (callInProgress) { + controls.push( + , ); } else { controls.push( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b2b4e01202..b5ecf26cb7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -50,12 +50,10 @@ "You cannot place a call with yourself.": "You cannot place a call with yourself.", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", - "End Call": "End Call", - "Remove the group call from the room?": "Remove the group call from the room?", - "Cancel": "Cancel", - "You don't have permission to remove the call from the room": "You don't have permission to remove the call from the room", "Permission Required": "Permission Required", "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", + "End conference": "End conference", + "Ending the conference will end the call for everyone. Continue?": "Ending the conference will end the call for everyone. Continue?", "Replying With Files": "Replying With Files", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?", "Continue": "Continue", @@ -143,6 +141,7 @@ "Cancel entering passphrase?": "Cancel entering passphrase?", "Are you sure you want to cancel entering passphrase?": "Are you sure you want to cancel entering passphrase?", "Go Back": "Go Back", + "Cancel": "Cancel", "Setting up keys": "Setting up keys", "Messages": "Messages", "Actions": "Actions", diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 10327ce4e9..be2233961b 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -22,6 +22,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import SettingsStore from "../settings/SettingsStore"; import WidgetEchoStore from "../stores/WidgetEchoStore"; +import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import WidgetUtils from "../utils/WidgetUtils"; import {SettingLevel} from "../settings/SettingLevel"; import {WidgetType} from "../widgets/WidgetType"; @@ -206,6 +207,24 @@ export default class WidgetStore extends AsyncStoreWithClient { } return roomInfo.widgets; } + + public doesRoomHaveConference(room: Room): boolean { + const roomInfo = this.getRoom(room.roomId); + if (!roomInfo) return false; + + const currentWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + const hasPendingWidgets = WidgetEchoStore.roomHasPendingWidgetsOfType(room.roomId, [], WidgetType.JITSI); + return currentWidgets.length > 0 || hasPendingWidgets; + } + + public isJoinedToConferenceIn(room: Room): boolean { + const roomInfo = this.getRoom(room.roomId); + if (!roomInfo) return false; + + // A persistent conference widget indicates that we're participating + const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + return widgets.some(w => ActiveWidgetStore.getWidgetPersistence(w.id)); + } } window.mxWidgetStore = WidgetStore.instance; diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 672cbf2a56..c25d607948 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -39,6 +39,7 @@ export enum KnownWidgetActions { SetAlwaysOnScreen = "set_always_on_screen", ClientReady = "im.vector.ready", Terminate = "im.vector.terminate", + Hangup = "im.vector.hangup", } export type WidgetAction = KnownWidgetActions | string; @@ -119,13 +120,15 @@ export class WidgetApi extends EventEmitter { // Automatically acknowledge so we can move on this.replyToRequest(payload, {}); - } else if (payload.action === KnownWidgetActions.Terminate) { + } else if (payload.action === KnownWidgetActions.Terminate + || payload.action === KnownWidgetActions.Hangup) { // Finalization needs to be async, so postpone with a promise let finalizePromise = Promise.resolve(); const wait = (promise) => { finalizePromise = finalizePromise.then(() => promise); }; - this.emit('terminate', wait); + const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup'; + this.emit(emitName, wait); Promise.resolve(finalizePromise).then(() => { // Acknowledge that we're shut down now this.replyToRequest(payload, {}); From f412f8defeab4a6af02722f3c91872b9857de83b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 14:59:15 -0600 Subject: [PATCH 04/17] Change copy if the widget is unpinned --- .../views/messages/MJitsiWidgetEvent.tsx | 22 +++++++++++++++---- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 1bfefbff6a..6f87aaec28 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -17,6 +17,8 @@ limitations under the License. import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; +import WidgetStore from "../../../stores/WidgetStore"; +import { WidgetType } from "../../../widgets/WidgetType"; interface IProps { mxEvent: MatrixEvent; @@ -36,12 +38,24 @@ export default class MJitsiWidgetEvent extends React.PureComponent WidgetType.JITSI.matches(w.type) && WidgetStore.instance.isPinned(w.id)); + + let joinCopy = _t('Join the conference at the top of this room'); + if (!isPinned) { + joinCopy = _t('Join the conference from the room information card on the right'); + } + if (!url) { // removed return (
- {_t("Video conference ended by %(senderName)s", {senderName})} + {_t('Video conference ended by %(senderName)s', {senderName})}
); @@ -50,10 +64,10 @@ export default class MJitsiWidgetEvent extends React.PureComponent
- {_t("Video conference updated by %(senderName)s", {senderName})} + {_t('Video conference updated by %(senderName)s', {senderName})}
- {_t("Join the conference at the top of this room.")} + {joinCopy}
); @@ -65,7 +79,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent
- {_t("Join the conference at the top of this room.")} + {joinCopy}
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 01d334505c..dc218aefc5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1402,9 +1402,10 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "Join the conference at the top of this room": "Join the conference at the top of this room", + "Join the conference from the room information card on the right": "Join the conference from the room information card on the right", "Video conference ended by %(senderName)s": "Video conference ended by %(senderName)s", "Video conference updated by %(senderName)s": "Video conference updated by %(senderName)s", - "Join the conference at the top of this room.": "Join the conference at the top of this room.", "Video conference started by %(senderName)s": "Video conference started by %(senderName)s", "You have ignored this user, so their message is hidden. Show anyways.": "You have ignored this user, so their message is hidden. Show anyways.", "You verified %(name)s": "You verified %(name)s", From 959b8dd31419003d598991785005d34c2d28255d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 14:59:40 -0600 Subject: [PATCH 05/17] de-state --- src/components/views/messages/MJitsiWidgetEvent.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 6f87aaec28..5171780ecc 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -24,13 +24,9 @@ interface IProps { mxEvent: MatrixEvent; } -interface IState { -} - -export default class MJitsiWidgetEvent extends React.PureComponent { +export default class MJitsiWidgetEvent extends React.PureComponent { constructor(props) { super(props); - this.state = {}; } render() { From dca48b984fa3440e2b78be33f984da459d16327e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 15:47:06 -0600 Subject: [PATCH 06/17] Be more sane --- src/components/views/messages/MJitsiWidgetEvent.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 5171780ecc..bd161b5ca2 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -34,15 +34,8 @@ export default class MJitsiWidgetEvent extends React.PureComponent { const prevUrl = this.props.mxEvent.getPrevContent()['url']; const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender(); - // XXX: We are assuming that there will only be one Jitsi widget per room, which isn't entirely - // safe but if there's more than 1 the user will be super confused anyways - the copy doesn't - // need to concern itself with this. - const roomInfo = WidgetStore.instance.getRoom(this.props.mxEvent.getRoomId()); - const isPinned = roomInfo?.widgets - .some(w => WidgetType.JITSI.matches(w.type) && WidgetStore.instance.isPinned(w.id)); - let joinCopy = _t('Join the conference at the top of this room'); - if (!isPinned) { + if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) { joinCopy = _t('Join the conference from the room information card on the right'); } From e52a02d733505d1a15e0957de9011f2296fffb77 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 17:26:00 -0600 Subject: [PATCH 07/17] Appease the linter --- src/components/views/messages/MJitsiWidgetEvent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index bd161b5ca2..3d191209f9 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import WidgetStore from "../../../stores/WidgetStore"; -import { WidgetType } from "../../../widgets/WidgetType"; interface IProps { mxEvent: MatrixEvent; From 8129333dcc35d4ea8cec32521489e1819cc52f5b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 22:38:12 -0600 Subject: [PATCH 08/17] Make the PIP Jitsi look and feel like the 1:1 PIP * Similar sizing * Fix pointers so the jitsi widget doesn't feel clickable when it's not * We might want to introduce click-to-visit-room for the Jitsi widget (like the 1:1 call), however the Jitsi widget has many more controls to worry about * Remove the menu bar from the widget to avoid accidents --- res/css/views/rooms/_AppsDrawer.scss | 4 ++-- res/css/views/voip/_CallContainer.scss | 14 ++++++++++++-- src/components/views/elements/PersistentApp.js | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index fee3d61153..b9249d310a 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$MiniAppTileHeight: 114px; +$MiniAppTileHeight: 200px; .mx_AppsDrawer { margin: 5px 5px 5px 18px; @@ -220,7 +220,7 @@ $MiniAppTileHeight: 114px; } .mx_AppTileBody_mini { - height: 112px; + height: $MiniAppTileHeight; width: 100%; overflow: hidden; } diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 4d26d8a312..650302b7e1 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -23,9 +23,16 @@ limitations under the License. z-index: 100; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - cursor: pointer; + // Disable pointer events for Jitsi widgets to function. Direct + // calls have their own cursor and behaviour, but we need to make + // sure the cursor hits the iframe for Jitsi which will be at a + // different level. + pointer-events: none; .mx_CallPreview { + pointer-events: initial; // restore pointer events so the user can leave/interact + cursor: pointer; + .mx_VideoView { width: 350px; } @@ -37,7 +44,7 @@ limitations under the License. } .mx_AppTile_persistedWrapper div { - min-width: 300px; + min-width: 350px; } .mx_IncomingCallBox { @@ -45,6 +52,9 @@ limitations under the License. background-color: $primary-bg-color; padding: 8px; + pointer-events: initial; // restore pointer events so the user can accept/decline + cursor: pointer; + .mx_IncomingCallBox_CallerInfo { display: flex; direction: row; diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index 686739a9f7..a3e413151a 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -82,6 +82,7 @@ export default class PersistentApp extends React.Component { showDelete={false} showMinimise={false} miniMode={true} + showMenubar={false} />; } } From e849cd8fe54466468ef6867d0546f43aafe57dc9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 18:13:52 -0600 Subject: [PATCH 09/17] Null-check the widget before continuing Deleted widgets should return isPinned=false --- src/stores/WidgetStore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 10327ce4e9..f3b8ee1299 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -158,7 +158,8 @@ export default class WidgetStore extends AsyncStoreWithClient { let pinned = roomInfo && roomInfo.pinned[widgetId]; // Jitsi widgets should be pinned by default - if (pinned === undefined && WidgetType.JITSI.matches(this.widgetMap.get(widgetId).type)) pinned = true; + const widget = this.widgetMap.get(widgetId); + if (pinned === undefined && WidgetType.JITSI.matches(widget?.type)) pinned = true; return pinned; } From 55ceb2abd6278b26b8a7d3cdf30ea0703c85088f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Sep 2020 09:33:05 -0600 Subject: [PATCH 10/17] speeeeeeling Co-authored-by: J. Ryan Stinnett --- src/WidgetMessaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 0f8626ec66..9394abf025 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -109,7 +109,7 @@ export default class WidgetMessaging { /** * Tells the widget to hang up on its call. - * @returns {Promise<*>} Resolves when teh widget has acknowledged the message. + * @returns {Promise<*>} Resolves when the widget has acknowledged the message. */ hangup() { return this.messageToWidget({ From 849a5e4a3976b7856e0c1efb998ed375c0a5887f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Sep 2020 14:58:48 -0600 Subject: [PATCH 11/17] Round the jitsi pip corners --- res/css/views/rooms/_AppsDrawer.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index b9249d310a..244e88ca3e 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -223,6 +223,7 @@ $MiniAppTileHeight: 200px; height: $MiniAppTileHeight; width: 100%; overflow: hidden; + border-radius: 8px; } .mx_AppTile .mx_AppTileBody, From feaa5f31eabd94cf34db78bd518a1c85ee31f7be Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Sep 2020 15:00:35 -0600 Subject: [PATCH 12/17] Match consistency --- src/CallHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index e40c97f025..3de1566234 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -318,7 +318,7 @@ function _onAction(payload) { break; case 'hangup_conference': console.info("Leaving conference call in %s", payload.room_id); - _hangupWithCallApp(payload.room_id); + _hangupCallApp(payload.room_id); break; case 'incoming_call': { @@ -450,7 +450,7 @@ function _terminateCallApp(roomId) { }); } -function _hangupWithCallApp(roomId) { +function _hangupCallApp(roomId) { const roomInfo = WidgetStore.instance.getRoom(roomId); if (!roomInfo) return; // "should never happen" clauses go here From a20d2af102fd4ad4661c1b6b2d9b0779086a6594 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 28 Sep 2020 13:53:44 -0600 Subject: [PATCH 13/17] Incorporate changes into new call handler --- src/CallHandler.tsx | 77 ++++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 62b91f938b..04f17b7216 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -74,6 +74,8 @@ import {base32} from "rfc4648"; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; +import WidgetStore from "./stores/WidgetStore"; +import ActiveWidgetStore from "./stores/ActiveWidgetStore"; // until we ts-ify the js-sdk voip code type Call = any; @@ -351,6 +353,14 @@ export default class CallHandler { console.info("Place conference call in %s", payload.room_id); this.startCallApp(payload.room_id, payload.type); break; + case 'end_conference': + console.info("Terminating conference call in %s", payload.room_id); + this.terminateCallApp(payload.room_id); + break; + case 'hangup_conference': + console.info("Leaving conference call in %s", payload.room_id); + this.hangupCallApp(payload.room_id); + break; case 'incoming_call': { if (this.getAnyActiveCall()) { @@ -398,10 +408,12 @@ export default class CallHandler { show: true, }); + // prevent double clicking the call button const room = MatrixClientPeg.get().getRoom(roomId); const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); - - if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) { + const hasJitsi = currentJitsiWidgets.length > 0 + || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI); + if (hasJitsi) { Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { title: _t('Call in Progress'), description: _t('A call is currently being placed!'), @@ -409,33 +421,6 @@ export default class CallHandler { return; } - if (currentJitsiWidgets.length > 0) { - console.warn( - "Refusing to start conference call widget in " + roomId + - " a conference call widget is already present", - ); - - if (WidgetUtils.canUserModifyWidgets(roomId)) { - Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, { - title: _t('End Call'), - description: _t('Remove the group call from the room?'), - button: _t('End Call'), - cancelButton: _t('Cancel'), - onFinished: (endCall) => { - if (endCall) { - WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']); - } - }, - }); - } else { - Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, { - title: _t('Call in Progress'), - description: _t("You don't have permission to remove the call from the room"), - }); - } - return; - } - const jitsiDomain = Jitsi.getInstance().preferredDomain; const jitsiAuth = await Jitsi.getInstance().getJitsiAuth(); let confId; @@ -484,4 +469,38 @@ export default class CallHandler { console.error(e); }); } + + private terminateCallApp(roomId: string) { + Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, { + hasCancelButton: true, + title: _t("End conference"), + description: _t("Ending the conference will end the call for everyone. Continue?"), + button: _t("End conference"), + onFinished: (proceed) => { + if (!proceed) return; + + // We'll just obliterate them all. There should only ever be one, but might as well + // be safe. + const roomInfo = WidgetStore.instance.getRoom(roomId); + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + // setting invalid content removes it + WidgetUtils.setRoomWidget(roomId, w.id); + }); + }, + }); + } + + private hangupCallApp(roomId: string) { + const roomInfo = WidgetStore.instance.getRoom(roomId); + if (!roomInfo) return; // "should never happen" clauses go here + + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + const messaging = ActiveWidgetStore.getWidgetMessaging(w.id); + if (!messaging) return; // more "should never happen" words + + messaging.hangup(); + }); + } } From ffa7ceb70e056d5914787c73c7906a140beeb0ff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 29 Sep 2020 14:15:20 +0100 Subject: [PATCH 14/17] Trim range when formatting so that it excludes leading/trailing spaces Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/BasicMessageComposer.tsx | 9 +++++---- src/editor/range.ts | 9 +++++++++ test/editor/range-test.js | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 7c2eb83a94..d9b34b93ef 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -619,13 +619,14 @@ export default class BasicMessageEditor extends React.Component } private onFormatAction = (action: Formatting) => { - const range = getRangeForSelection( - this.editorRef.current, - this.props.model, - document.getSelection()); + const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); + // trim the range as we want it to exclude leading/trailing spaces + range.trim(); + if (range.length === 0) { return; } + this.historyManager.ensureLastChangesPushed(this.props.model); this.modifiedFlag = true; switch (action) { diff --git a/src/editor/range.ts b/src/editor/range.ts index 27f59f34a9..1d9c75e238 100644 --- a/src/editor/range.ts +++ b/src/editor/range.ts @@ -18,6 +18,10 @@ import EditorModel from "./model"; import DocumentPosition, {Predicate} from "./position"; import {Part} from "./parts"; +const whileSpacePredicate: Predicate = (index, offset, part) => { + return part.text[offset] === " "; +}; + export default class Range { private _start: DocumentPosition; private _end: DocumentPosition; @@ -35,6 +39,11 @@ export default class Range { }); } + trim() { + this._start = this._start.forwardsWhile(this.model, whileSpacePredicate); + this._end = this._end.backwardsWhile(this.model, whileSpacePredicate); + } + expandBackwardsWhile(predicate: Predicate) { this._start = this._start.backwardsWhile(this.model, predicate); } diff --git a/test/editor/range-test.js b/test/editor/range-test.js index b69ed9eb53..60055af824 100644 --- a/test/editor/range-test.js +++ b/test/editor/range-test.js @@ -88,4 +88,19 @@ describe('editor/range', function() { expect(model.parts[1].text).toBe("man"); expect(model.parts.length).toBe(2); }); + it('range trim spaces off both ends', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("abc abc abc"), + ], pc, renderer); + const range = model.startRange( + model.positionForOffset(3, false), // at end of first `abc` + model.positionForOffset(8, false), // at start of last `abc` + ); + + expect(range.parts[0].text).toBe(" abc "); + range.trim(); + expect(range.parts[0].text).toBe("abc"); + }); }); From af4c95e267809efefa268d031dd193f4f2282a3e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 29 Sep 2020 14:17:44 +0100 Subject: [PATCH 15/17] apply to whitespace in general Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/editor/range.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor/range.ts b/src/editor/range.ts index 1d9c75e238..838dfd8b98 100644 --- a/src/editor/range.ts +++ b/src/editor/range.ts @@ -18,8 +18,8 @@ import EditorModel from "./model"; import DocumentPosition, {Predicate} from "./position"; import {Part} from "./parts"; -const whileSpacePredicate: Predicate = (index, offset, part) => { - return part.text[offset] === " "; +const whitespacePredicate: Predicate = (index, offset, part) => { + return part.text[offset].trim() === ""; }; export default class Range { @@ -40,8 +40,8 @@ export default class Range { } trim() { - this._start = this._start.forwardsWhile(this.model, whileSpacePredicate); - this._end = this._end.backwardsWhile(this.model, whileSpacePredicate); + this._start = this._start.forwardsWhile(this.model, whitespacePredicate); + this._end = this._end.backwardsWhile(this.model, whitespacePredicate); } expandBackwardsWhile(predicate: Predicate) { From 76a9803c6cf4212055c786ba870262f0a59bbafa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 29 Sep 2020 14:24:01 +0100 Subject: [PATCH 16/17] Fix button label on the Set Password Dialog Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/SetPasswordDialog.js | 4 +++- src/components/views/settings/ChangePassword.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js index 3649190ac9..f2d5a96b4c 100644 --- a/src/components/views/dialogs/SetPasswordDialog.js +++ b/src/components/views/dialogs/SetPasswordDialog.js @@ -117,7 +117,9 @@ export default class SetPasswordDialog extends React.Component { autoFocusNewPasswordInput={true} shouldAskForEmail={true} onError={this._onPasswordChangeError} - onFinished={this._onPasswordChanged} /> + onFinished={this._onPasswordChanged} + buttonLabel={_t("Set Password")} + />
{ this.state.error }
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 0b62f1fa81..8ae000f087 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -35,6 +35,7 @@ export default class ChangePassword extends React.Component { rowClassName: PropTypes.string, buttonClassName: PropTypes.string, buttonKind: PropTypes.string, + buttonLabel: PropTypes.string, confirm: PropTypes.bool, // Whether to autoFocus the new password input autoFocusNewPasswordInput: PropTypes.bool, @@ -271,7 +272,7 @@ export default class ChangePassword extends React.Component { /> - { _t('Change Password') } + { this.props.buttonLabel || _t('Change Password') } ); From bfa269a8487a4c3093aabc541eebe015881cba2c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 29 Sep 2020 10:20:54 -0600 Subject: [PATCH 17/17] Update copy --- src/CallHandler.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 04f17b7216..2ff018d4d6 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -474,7 +474,7 @@ export default class CallHandler { Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, { hasCancelButton: true, title: _t("End conference"), - description: _t("Ending the conference will end the call for everyone. Continue?"), + description: _t("This will end the conference for everyone. Continue?"), button: _t("End conference"), onFinished: (proceed) => { if (!proceed) return; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 06286adc90..cd31e18b0b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -53,7 +53,7 @@ "Permission Required": "Permission Required", "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "End conference": "End conference", - "Ending the conference will end the call for everyone. Continue?": "Ending the conference will end the call for everyone. Continue?", + "This will end the conference for everyone. Continue?": "This will end the conference for everyone. Continue?", "Replying With Files": "Replying With Files", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?", "Continue": "Continue",