diff --git a/res/css/_components.scss b/res/css/_components.scss index 4c253c7d9a..261b35690e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -141,6 +141,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 f9cda23650..34d40bf1fd 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -18,7 +18,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) { @@ -464,10 +463,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) { @@ -493,24 +488,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/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx new file mode 100644 index 0000000000..3d191209f9 --- /dev/null +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -0,0 +1,76 @@ +/* +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"; +import WidgetStore from "../../../stores/WidgetStore"; + +interface IProps { + mxEvent: MatrixEvent; +} + +export default class MJitsiWidgetEvent extends React.PureComponent { + constructor(props) { + super(props); + } + + 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(); + + let joinCopy = _t('Join the conference at the top of this room'); + if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) { + 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})} +
+
+ ); + } else if (prevUrl) { + // modified + return ( +
+
+ {_t('Video conference updated by %(senderName)s', {senderName})} +
+
+ {joinCopy} +
+
+ ); + } else { + // assume added + return ( +
+
+ {_t("Video conference started by %(senderName)s", {senderName})} +
+
+ {joinCopy} +
+
+ ); + } + } +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index a1cc681a4c..81034cf07b 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"; import RoomAvatar from "../avatars/RoomAvatar"; const eventTileTypes = { @@ -111,6 +112,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]; } @@ -627,16 +641,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 65374ea3ec..62d3e22d08 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -277,9 +277,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", @@ -1393,6 +1390,11 @@ "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", + "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", 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; }