From b4af0140d425c03ffe2e8044edf30f1fe8326d7a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 12:38:47 -0600 Subject: [PATCH 1/7] 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 2/7] 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 f412f8defeab4a6af02722f3c91872b9857de83b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 14:59:15 -0600 Subject: [PATCH 3/7] 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 4/7] 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 5/7] 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 6/7] 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 e849cd8fe54466468ef6867d0546f43aafe57dc9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 18:13:52 -0600 Subject: [PATCH 7/7] 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; }