From 73c66c36dd540dbc7da74f6532b9445b8a4124e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 29 May 2021 21:16:02 +0200 Subject: [PATCH 001/148] Add basic CallEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/CallEvent.tsx | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/components/views/messages/CallEvent.tsx diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx new file mode 100644 index 0000000000..42b3ce6b0f --- /dev/null +++ b/src/components/views/messages/CallEvent.tsx @@ -0,0 +1,59 @@ +/* +Copyright 2021 Šimon Brandner + +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 RoomCreate extends React.Component { + private isVoice(): boolean { + const event = this.props.mxEvent; + + // FIXME: Find a better way to determine this from the event? + let isVoice = true; + if (event.getContent().offer && event.getContent().offer.sdp && + event.getContent().offer.sdp.indexOf('m=video') !== -1) { + isVoice = false; + } + + return isVoice; + } + + render() { + const event = this.props.mxEvent; + const sender = event.sender ? event.sender.name : event.getSender(); + + return ( +
+
+ {sender} +
+
+ { this.isVoice() ? _t("Voice call") : _t("Video call") } +
+
+ ); + } +} From eaa3645238cf9b2f1b65cdd0c57181a702075f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 29 May 2021 21:16:25 +0200 Subject: [PATCH 002/148] Hook up CallEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/EventTile.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 67df5a84ba..71a7e39eba 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -52,10 +52,7 @@ const eventTileTypes = { [EventType.Sticker]: 'messages.MessageEvent', [EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion', [EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion', - [EventType.CallInvite]: 'messages.TextualEvent', - [EventType.CallAnswer]: 'messages.TextualEvent', - [EventType.CallHangup]: 'messages.TextualEvent', - [EventType.CallReject]: 'messages.TextualEvent', + [EventType.CallInvite]: 'messages.CallEvent', }; const stateEventTileTypes = { @@ -821,6 +818,7 @@ export default class EventTile extends React.Component { (eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) || (eventType === EventType.RoomCreate) || (eventType === EventType.RoomEncryption) || + (eventType === EventType.CallInvite) || (tileHandler === "messages.MJitsiWidgetEvent"); let isInfoMessage = ( !isBubbleMessage && eventType !== EventType.RoomMessage && From cd67d50a85c668f3d1548875e8526e03852b69c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 29 May 2021 21:22:58 +0200 Subject: [PATCH 003/148] Add basic CallEvent styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 res/css/views/messages/_CallEvent.scss diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss new file mode 100644 index 0000000000..cc465555e8 --- /dev/null +++ b/res/css/views/messages/_CallEvent.scss @@ -0,0 +1,33 @@ +/* +Copyright 2021 Šimon Brandner + +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_CallEvent { + display: flex; + flex-direction: column; + + background-color: $dark-panel-bg-color; + padding: 10px; + border-radius: 8px; + margin: 10px auto; + max-width: 75%; + box-sizing: border-box; + + .mx_CallEvent_sender {} + + .mx_CallEvent_type { + + } +} From 3ac63b03a63a885ea21e0237a16927dd02a7e5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 29 May 2021 21:23:15 +0200 Subject: [PATCH 004/148] Use styling for CallEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_components.scss | 1 + src/components/views/messages/CallEvent.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/_components.scss b/res/css/_components.scss index c8985cbb51..8a6f9a9ab1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -156,6 +156,7 @@ @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_EventTileBubble.scss"; +@import "./views/messages/_CallEvent.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 42b3ce6b0f..37a624222d 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -46,7 +46,7 @@ export default class RoomCreate extends React.Component { const sender = event.sender ? event.sender.name : event.getSender(); return ( -
+
{sender}
From 320ceb50364c356561b4253afa6041c528bd68b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 12:41:56 +0200 Subject: [PATCH 005/148] Add POC TimelineCallEventStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/TimelineCallEventStore.ts | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/stores/TimelineCallEventStore.ts diff --git a/src/stores/TimelineCallEventStore.ts b/src/stores/TimelineCallEventStore.ts new file mode 100644 index 0000000000..4c2acbd34c --- /dev/null +++ b/src/stores/TimelineCallEventStore.ts @@ -0,0 +1,95 @@ +/* +Copyright 2021 Šimon Brandner + +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 EventEmitter from "events"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +const IGNORED_EVENTS = [ + EventType.CallNegotiate, + EventType.CallCandidates, +]; + +export enum TimelineCallEventStoreEvent { + CallsChanged = "calls_changed" +} + +export enum TimelineCallState { + Invite = "invited", + Answered = "answered", + Ended = "ended", + Rejected = "rejected", + Unknown = "unknown" +} + +const EVENT_TYPE_TO_TIMELINE_CALL_STATE = new Map([ + [EventType.CallInvite, TimelineCallState.Invite], + [EventType.CallSelectAnswer, TimelineCallState.Answered], + [EventType.CallHangup, TimelineCallState.Ended], + [EventType.CallReject, TimelineCallState.Rejected], +]); + +export interface TimelineCall { + state: TimelineCallState; + date: Date; +} + +/** + * This gathers call events and creates objects for them accordingly, these can then be retrieved by CallEvent + */ +export default class TimelineCallEventStore extends EventEmitter { + private calls: Map = new Map(); + private static internalInstance: TimelineCallEventStore; + + public static get instance(): TimelineCallEventStore { + if (!TimelineCallEventStore.internalInstance) { + TimelineCallEventStore.internalInstance = new TimelineCallEventStore; + } + + return TimelineCallEventStore.internalInstance; + } + + public clear() { + this.calls.clear(); + } + + public getInfoByCallId(callId: string): TimelineCall { + return this.calls.get(callId); + } + + private getCallState(type: EventType): TimelineCallState { + return EVENT_TYPE_TO_TIMELINE_CALL_STATE.get(type); + } + + public addEvent(event: MatrixEvent) { + if (IGNORED_EVENTS.includes(event.getType())) return; + + const callId = event.getContent().call_id; + const date = event.getDate(); + const state = this.getCallState(event.getType()); + + + if (date < this.calls.get(callId)?.date) return; + if (!state) return; + + this.calls.set(callId, { + state: state, + date: date, + }); + + this.emit(TimelineCallEventStoreEvent.CallsChanged, this.calls) + } +} From 4ae92d8adc4b5d27695c3fccb0486f4c21bc0c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 12:42:23 +0200 Subject: [PATCH 006/148] Hook up TimelineCallEventStore and add Avatar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 14 ++++-- src/components/structures/MessagePanel.js | 3 ++ src/components/views/messages/CallEvent.tsx | 54 ++++++++++++++++++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index cc465555e8..dfff484734 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -16,7 +16,8 @@ limitations under the License. .mx_CallEvent { display: flex; - flex-direction: column; + flex-direction: row; + align-items: center; background-color: $dark-panel-bg-color; padding: 10px; @@ -25,9 +26,16 @@ limitations under the License. max-width: 75%; box-sizing: border-box; - .mx_CallEvent_sender {} + .mx_CallEvent_content { + display: flex; + flex-direction: column; - .mx_CallEvent_type { + .mx_CallEvent_sender { + } + + .mx_CallEvent_type { + + } } } diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index d1071a9e19..e2bb3135cf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -26,6 +26,7 @@ import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; +import TimelineCallEventStore from "../../stores/TimelineCallEventStore"; import {Layout, LayoutPropType} from "../../settings/Layout"; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; @@ -529,6 +530,8 @@ export default class MessagePanel extends React.Component { const last = (mxEv === lastShownEvent); const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i); + TimelineCallEventStore.instance.addEvent(mxEv); + if (grouper) { if (grouper.shouldGroup(mxEv)) { grouper.add(mxEv); diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 37a624222d..e8e6642776 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -18,16 +18,45 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from '../../../languageHandler'; +import TimelineCallEventStore, { + TimelineCall as TimelineCallSt, + TimelineCallEventStoreEvent, + TimelineCallState, +} from "../../../stores/TimelineCallEventStore"; +import MemberAvatar from '../avatars/MemberAvatar'; interface IProps { mxEvent: MatrixEvent; } interface IState { - + callState: TimelineCallState; } -export default class RoomCreate extends React.Component { +export default class CallEvent extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + callState: null, + } + } + + componentDidMount() { + TimelineCallEventStore.instance.addListener(TimelineCallEventStoreEvent.CallsChanged, this.onCallsChanged); + } + + componentWillUnmount() { + TimelineCallEventStore.instance.removeListener(TimelineCallEventStoreEvent.CallsChanged, this.onCallsChanged); + } + + private onCallsChanged = (calls: Map) => { + const callId = this.props.mxEvent.getContent().call_id; + const call = calls.get(callId); + if (!call) return; + this.setState({callState: call.state}); + } + private isVoice(): boolean { const event = this.props.mxEvent; @@ -44,14 +73,23 @@ export default class RoomCreate extends React.Component { render() { const event = this.props.mxEvent; const sender = event.sender ? event.sender.name : event.getSender(); + const state = this.state.callState; return ( -
-
- {sender} -
-
- { this.isVoice() ? _t("Voice call") : _t("Video call") } +
+ +
+
+ {sender} +
+
+ { this.isVoice() ? _t("Voice call") : _t("Video call") } + { state ? state : TimelineCallState.Unknown } +
); From 31d16d4277d2b8cd7d1821d95db47d81320f4e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 15:06:54 +0200 Subject: [PATCH 007/148] Fix ignoring events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/TimelineCallEventStore.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/stores/TimelineCallEventStore.ts b/src/stores/TimelineCallEventStore.ts index 4c2acbd34c..4689183adf 100644 --- a/src/stores/TimelineCallEventStore.ts +++ b/src/stores/TimelineCallEventStore.ts @@ -18,11 +18,6 @@ import EventEmitter from "events"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -const IGNORED_EVENTS = [ - EventType.CallNegotiate, - EventType.CallCandidates, -]; - export enum TimelineCallEventStoreEvent { CallsChanged = "calls_changed" } @@ -75,7 +70,7 @@ export default class TimelineCallEventStore extends EventEmitter { } public addEvent(event: MatrixEvent) { - if (IGNORED_EVENTS.includes(event.getType())) return; + if (!Array.from(EVENT_TYPE_TO_TIMELINE_CALL_STATE.keys()).includes(event.getType())) return; const callId = event.getContent().call_id; const date = event.getDate(); From 8dc0e2a7abd52c11f7a8253a56879b89b3c6d0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 16:26:46 +0200 Subject: [PATCH 008/148] Add CallEventGrouper as a replacement for TimeLineCallEventStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../structures/CallEventGrouper.tsx | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/components/structures/CallEventGrouper.tsx diff --git a/src/components/structures/CallEventGrouper.tsx b/src/components/structures/CallEventGrouper.tsx new file mode 100644 index 0000000000..5bc2fb4a03 --- /dev/null +++ b/src/components/structures/CallEventGrouper.tsx @@ -0,0 +1,53 @@ +/* +Copyright 2021 Šimon Brandner + +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 { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +export interface TimelineCallState { + callId?: string; + isVoice: boolean; +} + +export default class CallEventGrouper { + invite: MatrixEvent; + + private isVoice(): boolean { + const invite = this.invite; + + // FIXME: Find a better way to determine this from the event? + let isVoice = true; + if ( + invite.getContent().offer && invite.getContent().offer.sdp && + invite.getContent().offer.sdp.indexOf('m=video') !== -1 + ) { + isVoice = false; + } + + return isVoice; + } + + public add(event: MatrixEvent) { + if (event.getType() === EventType.CallInvite) this.invite = event; + } + + public getState(): TimelineCallState { + return { + isVoice: this.isVoice(), + } + } +} From 85bcf8ed521e380718f6f018a7da75ee3b0c9208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 16:28:30 +0200 Subject: [PATCH 009/148] Hook up CallEventGrouper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MessagePanel.js | 23 +++++- src/components/views/messages/CallEvent.tsx | 51 +----------- src/components/views/rooms/EventTile.tsx | 5 ++ src/stores/TimelineCallEventStore.ts | 90 --------------------- 4 files changed, 29 insertions(+), 140 deletions(-) delete mode 100644 src/stores/TimelineCallEventStore.ts diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index e2bb3135cf..ab5fe01e47 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -26,7 +26,6 @@ import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; -import TimelineCallEventStore from "../../stores/TimelineCallEventStore"; import {Layout, LayoutPropType} from "../../settings/Layout"; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; @@ -36,6 +35,7 @@ import DMRoomMap from "../../utils/DMRoomMap"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; import {replaceableComponent} from "../../utils/replaceableComponent"; import defaultDispatcher from '../../dispatcher/dispatcher'; +import CallEventGrouper from "./CallEventGrouper"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -210,6 +210,9 @@ export default class MessagePanel extends React.Component { this._showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + + // A map of + this._callEventGroupers = new Map(); } componentDidMount() { @@ -530,7 +533,20 @@ export default class MessagePanel extends React.Component { const last = (mxEv === lastShownEvent); const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i); - TimelineCallEventStore.instance.addEvent(mxEv); + if ( + mxEv.getType().indexOf("m.call.") === 0 || + mxEv.getType().indexOf("org.matrix.call.") === 0 + ) { + const callId = mxEv.getContent().call_id; + if (this._callEventGroupers.has(callId)) { + this._callEventGroupers.get(callId).add(mxEv); + } else { + const callEventGrouper = new CallEventGrouper(); + callEventGrouper.add(mxEv); + + this._callEventGroupers.set(callId, callEventGrouper); + } + } if (grouper) { if (grouper.shouldGroup(mxEv)) { @@ -646,6 +662,8 @@ export default class MessagePanel extends React.Component { // it's successful: we received it. isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); + const callState = this._callEventGroupers.get(mxEv.getContent().call_id)?.getState(); + // use txnId as key if available so that we don't remount during sending ret.push(
  • , diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index e8e6642776..182645c048 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -18,62 +18,18 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from '../../../languageHandler'; -import TimelineCallEventStore, { - TimelineCall as TimelineCallSt, - TimelineCallEventStoreEvent, - TimelineCallState, -} from "../../../stores/TimelineCallEventStore"; import MemberAvatar from '../avatars/MemberAvatar'; +import { TimelineCallState } from '../../structures/CallEventGrouper'; interface IProps { mxEvent: MatrixEvent; -} - -interface IState { callState: TimelineCallState; } -export default class CallEvent extends React.Component { - constructor(props: IProps) { - super(props); - - this.state = { - callState: null, - } - } - - componentDidMount() { - TimelineCallEventStore.instance.addListener(TimelineCallEventStoreEvent.CallsChanged, this.onCallsChanged); - } - - componentWillUnmount() { - TimelineCallEventStore.instance.removeListener(TimelineCallEventStoreEvent.CallsChanged, this.onCallsChanged); - } - - private onCallsChanged = (calls: Map) => { - const callId = this.props.mxEvent.getContent().call_id; - const call = calls.get(callId); - if (!call) return; - this.setState({callState: call.state}); - } - - private isVoice(): boolean { - const event = this.props.mxEvent; - - // FIXME: Find a better way to determine this from the event? - let isVoice = true; - if (event.getContent().offer && event.getContent().offer.sdp && - event.getContent().offer.sdp.indexOf('m=video') !== -1) { - isVoice = false; - } - - return isVoice; - } - +export default class CallEvent extends React.Component { render() { const event = this.props.mxEvent; const sender = event.sender ? event.sender.name : event.getSender(); - const state = this.state.callState; return (
    @@ -87,8 +43,7 @@ export default class CallEvent extends React.Component { {sender}
    - { this.isVoice() ? _t("Voice call") : _t("Video call") } - { state ? state : TimelineCallState.Unknown } + { this.props.callState.isVoice ? _t("Voice call") : _t("Video call") }
    diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 71a7e39eba..eb76354975 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -46,6 +46,7 @@ import { EditorStateTransfer } from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "./NotificationBadge"; +import { TimelineCallState } from "../../structures/CallEventGrouper"; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -274,6 +275,9 @@ interface IProps { // Helper to build permalinks for the room permalinkCreator?: RoomPermalinkCreator; + + // CallEventGrouper for this event + callState?: TimelineCallState; } interface IState { @@ -1139,6 +1143,7 @@ export default class EventTile extends React.Component { showUrlPreview={this.props.showUrlPreview} permalinkCreator={this.props.permalinkCreator} onHeightChanged={this.props.onHeightChanged} + callState={this.props.callState} /> { keyRequestInfo } { reactionsRow } diff --git a/src/stores/TimelineCallEventStore.ts b/src/stores/TimelineCallEventStore.ts deleted file mode 100644 index 4689183adf..0000000000 --- a/src/stores/TimelineCallEventStore.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2021 Šimon Brandner - -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 EventEmitter from "events"; -import { EventType } from "matrix-js-sdk/src/@types/event"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; - -export enum TimelineCallEventStoreEvent { - CallsChanged = "calls_changed" -} - -export enum TimelineCallState { - Invite = "invited", - Answered = "answered", - Ended = "ended", - Rejected = "rejected", - Unknown = "unknown" -} - -const EVENT_TYPE_TO_TIMELINE_CALL_STATE = new Map([ - [EventType.CallInvite, TimelineCallState.Invite], - [EventType.CallSelectAnswer, TimelineCallState.Answered], - [EventType.CallHangup, TimelineCallState.Ended], - [EventType.CallReject, TimelineCallState.Rejected], -]); - -export interface TimelineCall { - state: TimelineCallState; - date: Date; -} - -/** - * This gathers call events and creates objects for them accordingly, these can then be retrieved by CallEvent - */ -export default class TimelineCallEventStore extends EventEmitter { - private calls: Map = new Map(); - private static internalInstance: TimelineCallEventStore; - - public static get instance(): TimelineCallEventStore { - if (!TimelineCallEventStore.internalInstance) { - TimelineCallEventStore.internalInstance = new TimelineCallEventStore; - } - - return TimelineCallEventStore.internalInstance; - } - - public clear() { - this.calls.clear(); - } - - public getInfoByCallId(callId: string): TimelineCall { - return this.calls.get(callId); - } - - private getCallState(type: EventType): TimelineCallState { - return EVENT_TYPE_TO_TIMELINE_CALL_STATE.get(type); - } - - public addEvent(event: MatrixEvent) { - if (!Array.from(EVENT_TYPE_TO_TIMELINE_CALL_STATE.keys()).includes(event.getType())) return; - - const callId = event.getContent().call_id; - const date = event.getDate(); - const state = this.getCallState(event.getType()); - - - if (date < this.calls.get(callId)?.date) return; - if (!state) return; - - this.calls.set(callId, { - state: state, - date: date, - }); - - this.emit(TimelineCallEventStoreEvent.CallsChanged, this.calls) - } -} From 5e8df0372490b6ce594642c4789d3553184030e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 16:56:53 +0200 Subject: [PATCH 010/148] Fix styling a bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 9 ++++----- src/components/views/messages/CallEvent.tsx | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index dfff484734..49ff5f08c0 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -29,13 +29,12 @@ limitations under the License. .mx_CallEvent_content { display: flex; flex-direction: column; - - .mx_CallEvent_sender { - - } + margin-right: 10px; // To match mx_CallEvent .mx_CallEvent_type { - + font-weight: 400; + color: gray; + line-height: $font-14px; } } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 182645c048..e419e87bd2 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -39,7 +39,7 @@ export default class CallEvent extends React.Component { height={32} />
    -
    +
    {sender}
    From f94230c29205e1fb0847641045ce936ca9c3d02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 16:59:24 +0200 Subject: [PATCH 011/148] Fix css MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 49ff5f08c0..907e99d3ea 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -29,7 +29,7 @@ limitations under the License. .mx_CallEvent_content { display: flex; flex-direction: column; - margin-right: 10px; // To match mx_CallEvent + margin-left: 10px; // To match mx_CallEvent .mx_CallEvent_type { font-weight: 400; From 20c5735e96cc6d2ef338bf3f42ea70bec60ae535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 17:00:33 +0200 Subject: [PATCH 012/148] Add getCallById() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 90a631ab7f..c9a237f300 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -301,6 +301,12 @@ export default class CallHandler extends EventEmitter { }, true); } + public getCallById(callId: string): MatrixCall { + for (const call of this.calls.values()) { + if (call.callId === callId) return call; + } + } + getCallForRoom(roomId: string): MatrixCall { return this.calls.get(roomId) || null; } From d05b1798b80266ea9ce7e05899d084d92cb938f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 30 May 2021 19:35:51 +0200 Subject: [PATCH 013/148] Add callId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/CallEventGrouper.tsx b/src/components/structures/CallEventGrouper.tsx index 5bc2fb4a03..3b6d18310c 100644 --- a/src/components/structures/CallEventGrouper.tsx +++ b/src/components/structures/CallEventGrouper.tsx @@ -19,12 +19,13 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; export interface TimelineCallState { - callId?: string; + callId: string; isVoice: boolean; } export default class CallEventGrouper { invite: MatrixEvent; + callId: string; private isVoice(): boolean { const invite = this.invite; @@ -43,11 +44,13 @@ export default class CallEventGrouper { public add(event: MatrixEvent) { if (event.getType() === EventType.CallInvite) this.invite = event; + this.callId = event.getContent().call_id; } public getState(): TimelineCallState { return { isVoice: this.isVoice(), + callId: this.callId, } } } From 5e4a10ab84d56f3b427738b17834cd34b997094d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 07:55:55 +0200 Subject: [PATCH 014/148] Reorganize HTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 19 +++++++++------ src/components/views/messages/CallEvent.tsx | 27 ++++++++++++--------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 907e99d3ea..683cbb7331 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -26,15 +26,20 @@ limitations under the License. max-width: 75%; box-sizing: border-box; - .mx_CallEvent_content { + .mx_CallEvent_info { display: flex; - flex-direction: column; - margin-left: 10px; // To match mx_CallEvent + flex-direction: row; - .mx_CallEvent_type { - font-weight: 400; - color: gray; - line-height: $font-14px; + .mx_CallEvent_info_basic { + display: flex; + flex-direction: column; + margin-left: 10px; // To match mx_CallEvent + + .mx_CallEvent_type { + font-weight: 400; + color: gray; + line-height: $font-14px; + } } } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index e419e87bd2..05b046f939 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -31,21 +31,26 @@ export default class CallEvent extends React.Component { const event = this.props.mxEvent; const sender = event.sender ? event.sender.name : event.getSender(); + let content; + return (
    - -
    -
    - {sender} -
    -
    - { this.props.callState.isVoice ? _t("Voice call") : _t("Video call") } +
    + +
    +
    + { sender } +
    +
    + { this.props.callState.isVoice ? _t("Voice call") : _t("Video call") } +
    + { content }
    ); } From 8eb24d0d747ba9f5c20ad1ce2a05a7ca0593d46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 08:01:34 +0200 Subject: [PATCH 015/148] Rename callState to timelineCallState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/CallEvent.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 05b046f939..68e153546f 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -23,7 +23,8 @@ import { TimelineCallState } from '../../structures/CallEventGrouper'; interface IProps { mxEvent: MatrixEvent; - callState: TimelineCallState; + timelineCallState: TimelineCallState; +} } export default class CallEvent extends React.Component { @@ -46,7 +47,7 @@ export default class CallEvent extends React.Component { { sender }
    - { this.props.callState.isVoice ? _t("Voice call") : _t("Video call") } + { this.props.timelineCallState.isVoice ? _t("Voice call") : _t("Video call") }
    From dac741d8b9b3cc6263d0e4f6068adf39663285a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 09:30:37 +0200 Subject: [PATCH 016/148] Another rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 90 +++++++++++++++++++ .../structures/CallEventGrouper.tsx | 56 ------------ src/components/structures/MessagePanel.js | 4 +- src/components/views/messages/CallEvent.tsx | 34 +++++-- src/components/views/rooms/EventTile.tsx | 6 +- 5 files changed, 124 insertions(+), 66 deletions(-) create mode 100644 src/components/structures/CallEventGrouper.ts delete mode 100644 src/components/structures/CallEventGrouper.tsx diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts new file mode 100644 index 0000000000..41e67f580d --- /dev/null +++ b/src/components/structures/CallEventGrouper.ts @@ -0,0 +1,90 @@ +/* +Copyright 2021 Šimon Brandner + +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 { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import CallHandler from '../../CallHandler'; +import { EventEmitter } from 'events'; + +export enum CallEventGrouperState { + Incoming = "incoming", + Ended = "ended", +} + +export enum CallEventGrouperEvent { + StateChanged = "state_changed", +} + +export default class CallEventGrouper extends EventEmitter { + invite: MatrixEvent; + call: MatrixCall; + state: CallEventGrouperState; + + public answerCall() { + this.call?.answer(); + } + + public rejectCall() { + this.call?.reject(); + } + + public callBack() { + + } + + public isVoice(): boolean { + const invite = this.invite; + + // FIXME: Find a better way to determine this from the event? + let isVoice = true; + if ( + invite.getContent().offer && invite.getContent().offer.sdp && + invite.getContent().offer.sdp.indexOf('m=video') !== -1 + ) { + isVoice = false; + } + + return isVoice; + } + + public getState() { + return this.state; + } + + private setCallListeners() { + this.call.addListener(CallEvent.State, this.setCallState); + } + + private setCallState = () => { + if (this.call?.state === CallState.Ringing) { + this.state = CallEventGrouperState.Incoming; + } + this.emit(CallEventGrouperEvent.StateChanged, this.state); + } + + public add(event: MatrixEvent) { + if (event.getType() === EventType.CallInvite) this.invite = event; + + if (this.call) return; + const callId = event.getContent().call_id; + this.call = CallHandler.sharedInstance().getCallById(callId); + if (!this.call) return; + this.setCallListeners(); + this.setCallState(); + } +} diff --git a/src/components/structures/CallEventGrouper.tsx b/src/components/structures/CallEventGrouper.tsx deleted file mode 100644 index 3b6d18310c..0000000000 --- a/src/components/structures/CallEventGrouper.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2021 Šimon Brandner - -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 { EventType } from "matrix-js-sdk/src/@types/event"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; - -export interface TimelineCallState { - callId: string; - isVoice: boolean; -} - -export default class CallEventGrouper { - invite: MatrixEvent; - callId: string; - - private isVoice(): boolean { - const invite = this.invite; - - // FIXME: Find a better way to determine this from the event? - let isVoice = true; - if ( - invite.getContent().offer && invite.getContent().offer.sdp && - invite.getContent().offer.sdp.indexOf('m=video') !== -1 - ) { - isVoice = false; - } - - return isVoice; - } - - public add(event: MatrixEvent) { - if (event.getType() === EventType.CallInvite) this.invite = event; - this.callId = event.getContent().call_id; - } - - public getState(): TimelineCallState { - return { - isVoice: this.isVoice(), - callId: this.callId, - } - } -} diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index ab5fe01e47..b6d9f619c8 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -662,7 +662,7 @@ export default class MessagePanel extends React.Component { // it's successful: we received it. isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); - const callState = this._callEventGroupers.get(mxEv.getContent().call_id)?.getState(); + const callEventGrouper = this._callEventGroupers.get(mxEv.getContent().call_id); // use txnId as key if available so that we don't remount during sending ret.push( @@ -696,7 +696,7 @@ export default class MessagePanel extends React.Component { layout={this.props.layout} enableFlair={this.props.enableFlair} showReadReceipts={this.props.showReadReceipts} - callState={callState} + callEventGrouper={callEventGrouper} /> , diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 68e153546f..88b1498272 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -19,15 +19,39 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; -import { TimelineCallState } from '../../structures/CallEventGrouper'; +import CallEventGrouper, { CallEventGrouperEvent, CallEventGrouperState } from '../../structures/CallEventGrouper'; +import FormButton from '../elements/FormButton'; interface IProps { mxEvent: MatrixEvent; - timelineCallState: TimelineCallState; -} + callEventGrouper: CallEventGrouper; } -export default class CallEvent extends React.Component { +interface IState { + callState: CallEventGrouperState; +} + +export default class CallEvent extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + callState: this.props.callEventGrouper.getState(), + } + } + + componentDidMount() { + this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); + } + + componentWillUnmount() { + this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); + } + + private onStateChanged = (newState: CallEventGrouperState) => { + this.setState({callState: newState}); + } + render() { const event = this.props.mxEvent; const sender = event.sender ? event.sender.name : event.getSender(); @@ -47,7 +71,7 @@ export default class CallEvent extends React.Component { { sender }
    - { this.props.timelineCallState.isVoice ? _t("Voice call") : _t("Video call") } + { this.props.callEventGrouper.isVoice() ? _t("Voice call") : _t("Video call") }
    diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index eb76354975..930be62fbf 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -46,7 +46,7 @@ import { EditorStateTransfer } from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "./NotificationBadge"; -import { TimelineCallState } from "../../structures/CallEventGrouper"; +import CallEventGrouper from "../../structures/CallEventGrouper"; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -277,7 +277,7 @@ interface IProps { permalinkCreator?: RoomPermalinkCreator; // CallEventGrouper for this event - callState?: TimelineCallState; + callEventGrouper?: CallEventGrouper; } interface IState { @@ -1143,7 +1143,7 @@ export default class EventTile extends React.Component { showUrlPreview={this.props.showUrlPreview} permalinkCreator={this.props.permalinkCreator} onHeightChanged={this.props.onHeightChanged} - callState={this.props.callState} + callEventGrouper={this.props.callEventGrouper} /> { keyRequestInfo } { reactionsRow } From 30365ca1ad2293562cc41ae926b483555353b6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 10:03:17 +0200 Subject: [PATCH 017/148] Allow picking up calls from the timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 6 +++--- src/components/views/messages/CallEvent.tsx | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 41e67f580d..ab89e48ec6 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -35,15 +35,15 @@ export default class CallEventGrouper extends EventEmitter { call: MatrixCall; state: CallEventGrouperState; - public answerCall() { + public answerCall = () => { this.call?.answer(); } - public rejectCall() { + public rejectCall = () => { this.call?.reject(); } - public callBack() { + public callBack = () => { } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 88b1498272..0806934420 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -57,6 +57,25 @@ export default class CallEvent extends React.Component { const sender = event.sender ? event.sender.name : event.getSender(); let content; + if (this.state.callState === CallEventGrouperState.Incoming) { + content = ( +
    + +
    + +
    + ); + } return (
    From 86402e9788fa5b9fb6f65db8263ced9e241a2387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 10:03:23 +0200 Subject: [PATCH 018/148] Add some styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 683cbb7331..e41cb7becf 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -18,6 +18,7 @@ limitations under the License. display: flex; flex-direction: row; align-items: center; + justify-content: space-between; background-color: $dark-panel-bg-color; padding: 10px; @@ -29,6 +30,7 @@ limitations under the License. .mx_CallEvent_info { display: flex; flex-direction: row; + align-items: center; .mx_CallEvent_info_basic { display: flex; @@ -42,4 +44,9 @@ limitations under the License. } } } + + .mx_CallEvent_content { + display: flex; + flex-direction: row; + } } From 6b72c13e34d8a3ea5d06d5823603f270b496d940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 10:06:03 +0200 Subject: [PATCH 019/148] Add some call states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index ab89e48ec6..2c08d7b047 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -23,6 +23,11 @@ import { EventEmitter } from 'events'; export enum CallEventGrouperState { Incoming = "incoming", + Connecting = "connecting", + Connected = "connected", + Ringing = "ringing", + Missed = "missed", + Rejected = "rejected", Ended = "ended", } From f96e25d833d04cdfa2c3fb53e3b5560e392d86e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 10:11:48 +0200 Subject: [PATCH 020/148] Simply use call states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 16 ++-------------- src/components/views/messages/CallEvent.tsx | 7 ++++--- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 2c08d7b047..5184ddc1bb 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -21,16 +21,6 @@ import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call" import CallHandler from '../../CallHandler'; import { EventEmitter } from 'events'; -export enum CallEventGrouperState { - Incoming = "incoming", - Connecting = "connecting", - Connected = "connected", - Ringing = "ringing", - Missed = "missed", - Rejected = "rejected", - Ended = "ended", -} - export enum CallEventGrouperEvent { StateChanged = "state_changed", } @@ -38,7 +28,7 @@ export enum CallEventGrouperEvent { export default class CallEventGrouper extends EventEmitter { invite: MatrixEvent; call: MatrixCall; - state: CallEventGrouperState; + state: CallState; public answerCall = () => { this.call?.answer(); @@ -76,9 +66,7 @@ export default class CallEventGrouper extends EventEmitter { } private setCallState = () => { - if (this.call?.state === CallState.Ringing) { - this.state = CallEventGrouperState.Incoming; - } + this.state = this.call.state this.emit(CallEventGrouperEvent.StateChanged, this.state); } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 0806934420..c4126639a7 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -19,8 +19,9 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; -import CallEventGrouper, { CallEventGrouperEvent, CallEventGrouperState } from '../../structures/CallEventGrouper'; +import CallEventGrouper, { CallEventGrouperEvent } from '../../structures/CallEventGrouper'; import FormButton from '../elements/FormButton'; +import { CallState } from 'matrix-js-sdk/src/webrtc/call'; interface IProps { mxEvent: MatrixEvent; @@ -28,7 +29,7 @@ interface IProps { } interface IState { - callState: CallEventGrouperState; + callState: CallState; } export default class CallEvent extends React.Component { @@ -57,7 +58,7 @@ export default class CallEvent extends React.Component { const sender = event.sender ? event.sender.name : event.getSender(); let content; - if (this.state.callState === CallEventGrouperState.Incoming) { + if (this.state.callState === CallState.Ringing) { content = (
    Date: Tue, 1 Jun 2021 10:33:44 +0200 Subject: [PATCH 021/148] Manage some more call states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 14 +++++++++-- src/components/views/messages/CallEvent.tsx | 23 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 5184ddc1bb..c654c08636 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -25,6 +25,13 @@ export enum CallEventGrouperEvent { StateChanged = "state_changed", } +const SUPPORTED_STATES = [ + CallState.Connected, + CallState.Connecting, + CallState.Ended, + CallState.Ringing, +]; + export default class CallEventGrouper extends EventEmitter { invite: MatrixEvent; call: MatrixCall; @@ -66,12 +73,15 @@ export default class CallEventGrouper extends EventEmitter { } private setCallState = () => { - this.state = this.call.state - this.emit(CallEventGrouperEvent.StateChanged, this.state); + if (SUPPORTED_STATES.includes(this.call.state)) { + this.state = this.call.state; + this.emit(CallEventGrouperEvent.StateChanged, this.state); + } } public add(event: MatrixEvent) { if (event.getType() === EventType.CallInvite) this.invite = event; + if (event.getType() === EventType.CallHangup) this.state = CallState.Ended; if (this.call) return; const callId = event.getContent().call_id; diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index c4126639a7..d5f26389c2 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -32,6 +32,12 @@ interface IState { callState: CallState; } +const TEXTUAL_STATES = new Map([ + [CallState.Connected, _t("Connected")], + [CallState.Connecting, _t("Connecting")], + [CallState.Ended, _t("This call has ended")], +]); + export default class CallEvent extends React.Component { constructor(props: IProps) { super(props); @@ -49,7 +55,7 @@ export default class CallEvent extends React.Component { this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); } - private onStateChanged = (newState: CallEventGrouperState) => { + private onStateChanged = (newState: CallState) => { this.setState({callState: newState}); } @@ -57,8 +63,9 @@ export default class CallEvent extends React.Component { const event = this.props.mxEvent; const sender = event.sender ? event.sender.name : event.getSender(); + const state = this.state.callState; let content; - if (this.state.callState === CallState.Ringing) { + if (state === CallState.Ringing) { content = (
    { />
    ); + } else if (Array.from(TEXTUAL_STATES.keys()).includes(state)) { + content = ( +
    + { TEXTUAL_STATES.get(state) } +
    + ); + } else { + content = ( +
    + { _t("The call is in an unknown state!") } +
    + ); } return ( From 8c67b96a0f3672edaea4e283b62f1caa777015e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 10:42:21 +0200 Subject: [PATCH 022/148] Save all events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index c654c08636..84f178b75f 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -33,10 +33,14 @@ const SUPPORTED_STATES = [ ]; export default class CallEventGrouper extends EventEmitter { - invite: MatrixEvent; + events: Array = []; call: MatrixCall; state: CallState; + private get invite(): MatrixEvent { + return this.events.find((event) => event.getType() === EventType.CallInvite); + } + public answerCall = () => { this.call?.answer(); } @@ -80,10 +84,11 @@ export default class CallEventGrouper extends EventEmitter { } public add(event: MatrixEvent) { - if (event.getType() === EventType.CallInvite) this.invite = event; - if (event.getType() === EventType.CallHangup) this.state = CallState.Ended; + this.events.push(event); + const type = event.getType(); - if (this.call) return; + if (type === EventType.CallHangup) this.state = CallState.Ended; + else if (type === EventType.CallReject) this.state = CallState.Ended; const callId = event.getContent().call_id; this.call = CallHandler.sharedInstance().getCallById(callId); if (!this.call) return; From 67a052e46ae9e40a175d84f9a702db8bd18e491c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 10:55:03 +0200 Subject: [PATCH 023/148] Reorganize things and do some fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 84f178b75f..5bf7d45f59 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -73,26 +73,30 @@ export default class CallEventGrouper extends EventEmitter { } private setCallListeners() { + if (!this.call) return; this.call.addListener(CallEvent.State, this.setCallState); } private setCallState = () => { - if (SUPPORTED_STATES.includes(this.call.state)) { + if (SUPPORTED_STATES.includes(this.call?.state)) { this.state = this.call.state; - this.emit(CallEventGrouperEvent.StateChanged, this.state); + } else { + const lastEvent = this.events[this.events.length - 1]; + const lastEventType = lastEvent.getType(); + + if (lastEventType === EventType.CallHangup) this.state = CallState.Ended; + else if (lastEventType === EventType.CallReject) this.state = CallState.Ended; } + this.emit(CallEventGrouperEvent.StateChanged, this.state); } public add(event: MatrixEvent) { - this.events.push(event); - const type = event.getType(); - - if (type === EventType.CallHangup) this.state = CallState.Ended; - else if (type === EventType.CallReject) this.state = CallState.Ended; const callId = event.getContent().call_id; - this.call = CallHandler.sharedInstance().getCallById(callId); - if (!this.call) return; - this.setCallListeners(); + this.events.push(event); + if (!this.call) { + this.call = CallHandler.sharedInstance().getCallById(callId); + this.setCallListeners(); + } this.setCallState(); } } From 79ec655e660aaf6b512f664abd996d3802727a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 10:58:17 +0200 Subject: [PATCH 024/148] Fix translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/CallEvent.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index d5f26389c2..a048faf6b0 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; import CallEventGrouper, { CallEventGrouperEvent } from '../../structures/CallEventGrouper'; import FormButton from '../elements/FormButton'; @@ -33,9 +33,9 @@ interface IState { } const TEXTUAL_STATES = new Map([ - [CallState.Connected, _t("Connected")], - [CallState.Connecting, _t("Connecting")], - [CallState.Ended, _t("This call has ended")], + [CallState.Connected, _td("Connected")], + [CallState.Connecting, _td("Connecting")], + [CallState.Ended, _td("This call has ended")], ]); export default class CallEvent extends React.Component { @@ -92,7 +92,7 @@ export default class CallEvent extends React.Component { } else { content = (
    - { _t("The call is in an unknown state!") } + { "The call is in an unknown state!" }
    ); } From 5b3967a486815fff26f508857ff5eb863a6ea3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 11:01:10 +0200 Subject: [PATCH 025/148] Add handling for invite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 5bf7d45f59..08f91f42bf 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -86,6 +86,7 @@ export default class CallEventGrouper extends EventEmitter { if (lastEventType === EventType.CallHangup) this.state = CallState.Ended; else if (lastEventType === EventType.CallReject) this.state = CallState.Ended; + else if (lastEventType === EventType.CallInvite) this.state = CallState.Connecting; } this.emit(CallEventGrouperEvent.StateChanged, this.state); } From 078599798374551e11f8511037f62b44f6977e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 11:28:45 +0200 Subject: [PATCH 026/148] Handle missed calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 5 ++++ src/components/structures/CallEventGrouper.ts | 23 +++++++++++++++---- src/components/views/messages/CallEvent.tsx | 21 ++++++++++++----- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index e41cb7becf..9f61295a5a 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -48,5 +48,10 @@ limitations under the License. .mx_CallEvent_content { display: flex; flex-direction: row; + align-items: center; + + .mx_CallEvent_content_callBack { + margin-left: 10px; // To match mx_callEvent + } } } diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 08f91f42bf..5a3e5720e3 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -17,9 +17,11 @@ limitations under the License. import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import CallHandler from '../../CallHandler'; import { EventEmitter } from 'events'; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import defaultDispatcher from "../../dispatcher/dispatcher"; export enum CallEventGrouperEvent { StateChanged = "state_changed", @@ -32,10 +34,14 @@ const SUPPORTED_STATES = [ CallState.Ringing, ]; +export enum CustomCallState { + Missed = "missed", +} + export default class CallEventGrouper extends EventEmitter { events: Array = []; call: MatrixCall; - state: CallState; + state: CallState | CustomCallState; private get invite(): MatrixEvent { return this.events.find((event) => event.getType() === EventType.CallInvite); @@ -50,7 +56,11 @@ export default class CallEventGrouper extends EventEmitter { } public callBack = () => { - + defaultDispatcher.dispatch({ + action: 'place_call', + type: this.isVoice ? CallType.Voice : CallType.Video, + room_id: this.events[0]?.getRoomId(), + }); } public isVoice(): boolean { @@ -68,7 +78,7 @@ export default class CallEventGrouper extends EventEmitter { return isVoice; } - public getState() { + public getState(): CallState | CustomCallState { return this.state; } @@ -86,7 +96,10 @@ export default class CallEventGrouper extends EventEmitter { if (lastEventType === EventType.CallHangup) this.state = CallState.Ended; else if (lastEventType === EventType.CallReject) this.state = CallState.Ended; - else if (lastEventType === EventType.CallInvite) this.state = CallState.Connecting; + else if (lastEventType === EventType.CallInvite && this.call) this.state = CallState.Connecting; + else if (this.invite?.sender?.userId !== MatrixClientPeg.get().getUserId()) { + this.state = CustomCallState.Missed; + } } this.emit(CallEventGrouperEvent.StateChanged, this.state); } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index a048faf6b0..fbc653a8ca 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t, _td } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; -import CallEventGrouper, { CallEventGrouperEvent } from '../../structures/CallEventGrouper'; +import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper'; import FormButton from '../elements/FormButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; @@ -29,10 +29,10 @@ interface IProps { } interface IState { - callState: CallState; + callState: CallState | CustomCallState; } -const TEXTUAL_STATES = new Map([ +const TEXTUAL_STATES: Map = new Map([ [CallState.Connected, _td("Connected")], [CallState.Connecting, _td("Connecting")], [CallState.Ended, _td("This call has ended")], @@ -69,14 +69,11 @@ export default class CallEvent extends React.Component { content = (
    -
    { { TEXTUAL_STATES.get(state) }
    ); + } else if (state === CustomCallState.Missed) { + content = ( +
    + { _t("You missed this call") } + +
    + ); } else { content = (
    From 79f51adf2534e449d53e51c8a201fee8184ff6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 11:38:17 +0200 Subject: [PATCH 027/148] Delete old call tile handlers that are replaced by CallEventGrouper and CallEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/TextForEvent.js | 76 --------------------------------------------- 1 file changed, 76 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 86f9ff20f4..e8e75e196f 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -288,78 +288,6 @@ function textForCanonicalAliasEvent(ev) { }); } -function textForCallAnswerEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); - const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); - return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported; -} - -function textForCallHangupEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); - const eventContent = event.getContent(); - let reason = ""; - if (!MatrixClientPeg.get().supportsVoip()) { - reason = _t('(not supported by this browser)'); - } else if (eventContent.reason) { - if (eventContent.reason === "ice_failed") { - // We couldn't establish a connection at all - reason = _t('(could not connect media)'); - } else if (eventContent.reason === "ice_timeout") { - // We established a connection but it died - reason = _t('(connection failed)'); - } else if (eventContent.reason === "user_media_failed") { - // The other side couldn't open capture devices - reason = _t("(their device couldn't start the camera / microphone)"); - } else if (eventContent.reason === "unknown_error") { - // An error code the other side doesn't have a way to express - // (as opposed to an error code they gave but we don't know about, - // in which case we show the error code) - reason = _t("(an error occurred)"); - } else if (eventContent.reason === "invite_timeout") { - reason = _t('(no answer)'); - } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { - // workaround for https://github.com/vector-im/element-web/issues/5178 - // it seems Android randomly sets a reason of "user hangup" which is - // interpreted as an error code :( - // https://github.com/vector-im/riot-android/issues/2623 - // Also the correct hangup code as of VoIP v1 (with underscore) - reason = ''; - } else { - reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); - } - } - return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason; -} - -function textForCallRejectEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); - return _t('%(senderName)s declined the call.', {senderName}); -} - -function textForCallInviteEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); - // FIXME: Find a better way to determine this from the event? - let isVoice = true; - if (event.getContent().offer && event.getContent().offer.sdp && - event.getContent().offer.sdp.indexOf('m=video') !== -1) { - isVoice = false; - } - const isSupported = MatrixClientPeg.get().supportsVoip(); - - // This ladder could be reduced down to a couple string variables, however other languages - // can have a hard time translating those strings. In an effort to make translations easier - // and more accurate, we break out the string-based variables to a couple booleans. - if (isVoice && isSupported) { - return _t("%(senderName)s placed a voice call.", {senderName}); - } else if (isVoice && !isSupported) { - return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName}); - } else if (!isVoice && isSupported) { - return _t("%(senderName)s placed a video call.", {senderName}); - } else if (!isVoice && !isSupported) { - return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName}); - } -} - function textForThreePidInviteEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); @@ -573,10 +501,6 @@ function textForMjolnirEvent(event) { const handlers = { 'm.room.message': textForMessageEvent, - 'm.call.invite': textForCallInviteEvent, - 'm.call.answer': textForCallAnswerEvent, - 'm.call.hangup': textForCallHangupEvent, - 'm.call.reject': textForCallRejectEvent, }; const stateHandlers = { From 527723c63045fa108953be195d379725f690db83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 11:51:05 +0200 Subject: [PATCH 028/148] Remove unused import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/TextForEvent.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index e8e75e196f..a89b282753 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -13,7 +13,6 @@ 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 {MatrixClientPeg} from './MatrixClientPeg'; import { _t } from './languageHandler'; import * as Roles from './Roles'; import {isValid3pidInvite} from "./RoomInvite"; From 91288ab5259701d0c5fe0497c89d5952a7239695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 11:51:34 +0200 Subject: [PATCH 029/148] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d6fcb8643..bcf1bdf6d7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -533,20 +533,6 @@ "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.", "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.", "Someone": "Someone", - "(not supported by this browser)": "(not supported by this browser)", - "%(senderName)s answered the call.": "%(senderName)s answered the call.", - "(could not connect media)": "(could not connect media)", - "(connection failed)": "(connection failed)", - "(their device couldn't start the camera / microphone)": "(their device couldn't start the camera / microphone)", - "(an error occurred)": "(an error occurred)", - "(no answer)": "(no answer)", - "(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)", - "%(senderName)s ended the call.": "%(senderName)s ended the call.", - "%(senderName)s declined the call.": "%(senderName)s declined the call.", - "%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.", - "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)", - "%(senderName)s placed a video call.": "%(senderName)s placed a video call.", - "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s placed a video call. (not supported by this browser)", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.", @@ -1813,6 +1799,10 @@ "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", "Compare emoji": "Compare emoji", + "Connected": "Connected", + "This call has ended": "This call has ended", + "You missed this call": "You missed this call", + "Call back": "Call back", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 9b904cdee897ed681d5a1c29941a29f3a8389e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 13:39:05 +0200 Subject: [PATCH 030/148] Remove empty line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MessagePanel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index b6d9f619c8..1b2025848c 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -543,7 +543,6 @@ export default class MessagePanel extends React.Component { } else { const callEventGrouper = new CallEventGrouper(); callEventGrouper.add(mxEv); - this._callEventGroupers.set(callId, callEventGrouper); } } From f1e780e6428b0c9f633e7039d50549da1acf3f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 13:40:25 +0200 Subject: [PATCH 031/148] Improved missed calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 5a3e5720e3..c53efadd7a 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -82,6 +82,13 @@ export default class CallEventGrouper extends EventEmitter { return this.state; } + /** + * Returns true if there are only events from the other side - we missed the call + */ + private wasThisCallMissed(): boolean { + return !this.events.some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); + } + private setCallListeners() { if (!this.call) return; this.call.addListener(CallEvent.State, this.setCallState); @@ -94,12 +101,10 @@ export default class CallEventGrouper extends EventEmitter { const lastEvent = this.events[this.events.length - 1]; const lastEventType = lastEvent.getType(); - if (lastEventType === EventType.CallHangup) this.state = CallState.Ended; + if (this.wasThisCallMissed()) this.state = CustomCallState.Missed; + else if (lastEventType === EventType.CallHangup) this.state = CallState.Ended; else if (lastEventType === EventType.CallReject) this.state = CallState.Ended; else if (lastEventType === EventType.CallInvite && this.call) this.state = CallState.Connecting; - else if (this.invite?.sender?.userId !== MatrixClientPeg.get().getUserId()) { - this.state = CustomCallState.Missed; - } } this.emit(CallEventGrouperEvent.StateChanged, this.state); } From 795dfa7206084a1b38d87cbddd269c6817fbfc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 14:28:00 +0200 Subject: [PATCH 032/148] Allow custom classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/InfoTooltip.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index d49090dbae..9eea0a96dc 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -24,6 +24,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; interface ITooltipProps { tooltip?: React.ReactNode; + className?: string, tooltipClassName?: string; } @@ -53,7 +54,7 @@ export default class InfoTooltip extends React.PureComponent :
    ; return ( -
    +
    {children} {tip} From 3a0b6eb466f0872af9bd917530dc02ef4fec635f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 14:40:08 +0200 Subject: [PATCH 033/148] Add a warning icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/img/element-icons/warning.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 res/img/element-icons/warning.svg diff --git a/res/img/element-icons/warning.svg b/res/img/element-icons/warning.svg new file mode 100644 index 0000000000..eef5193140 --- /dev/null +++ b/res/img/element-icons/warning.svg @@ -0,0 +1,3 @@ + + + From 2a22f03a6ac920b4808c84e5107ade2badbe7595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 14:40:27 +0200 Subject: [PATCH 034/148] Support InfoTooltip kinds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_InfoTooltip.scss | 7 +++++++ src/components/views/elements/InfoTooltip.tsx | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss index 5858a60629..5329e7f1f8 100644 --- a/res/css/views/elements/_InfoTooltip.scss +++ b/res/css/views/elements/_InfoTooltip.scss @@ -30,5 +30,12 @@ limitations under the License. mask-position: center; content: ''; vertical-align: middle; +} + +.mx_InfoTooltip_icon_info::before { mask-image: url('$(res)/img/element-icons/info.svg'); } + +.mx_InfoTooltip_icon_warning::before { + mask-image: url('$(res)/img/element-icons/warning.svg'); +} diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index 9eea0a96dc..ca592b1849 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -22,10 +22,16 @@ import Tooltip, {Alignment} from './Tooltip'; import {_t} from "../../../languageHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +export enum InfoTooltipKind { + Info = "info", + Warning = "warning", +} + interface ITooltipProps { tooltip?: React.ReactNode; className?: string, tooltipClassName?: string; + kind?: InfoTooltipKind; } interface IState { @@ -54,8 +60,12 @@ export default class InfoTooltip extends React.PureComponent - + {children} {tip}
    From 70a5715b3d79438588b048692f4e9ad5f0fa1e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 14:46:41 +0200 Subject: [PATCH 035/148] Support hangup reasons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 4 + src/components/structures/CallEventGrouper.ts | 4 + src/components/views/messages/CallEvent.tsx | 104 +++++++++++++----- 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 9f61295a5a..2e36daccfa 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -53,5 +53,9 @@ limitations under the License. .mx_CallEvent_content_callBack { margin-left: 10px; // To match mx_callEvent } + + .mx_CallEvent_content_tooltip { + margin-right: 5px; + } } } diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index c53efadd7a..15de2dcaf7 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -82,6 +82,10 @@ export default class CallEventGrouper extends EventEmitter { return this.state; } + public getHangupReason(): string | null { + return this.events.find((event) => event.getType() === EventType.CallHangup)?.getContent()?.reason; + } + /** * Returns true if there are only events from the other side - we missed the call */ diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index fbc653a8ca..a4c0d02797 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper'; import FormButton from '../elements/FormButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; +import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; interface IProps { mxEvent: MatrixEvent; @@ -35,7 +36,6 @@ interface IState { const TEXTUAL_STATES: Map = new Map([ [CallState.Connected, _td("Connected")], [CallState.Connecting, _td("Connecting")], - [CallState.Ended, _td("This call has ended")], ]); export default class CallEvent extends React.Component { @@ -59,53 +59,107 @@ export default class CallEvent extends React.Component { this.setState({callState: newState}); } - render() { - const event = this.props.mxEvent; - const sender = event.sender ? event.sender.name : event.getSender(); - - const state = this.state.callState; - let content; + private renderContent(state: CallState | CustomCallState): JSX.Element { if (state === CallState.Ringing) { - content = ( + return (
    ); - } else if (Array.from(TEXTUAL_STATES.keys()).includes(state)) { - content = ( + } + if (state === CallState.Ended) { + const hangupReason = this.props.callEventGrouper.getHangupReason(); + + if (["user_hangup", "user hangup"].includes(hangupReason) || !hangupReason) { + // workaround for https://github.com/vector-im/element-web/issues/5178 + // it seems Android randomly sets a reason of "user hangup" which is + // interpreted as an error code :( + // https://github.com/vector-im/riot-android/issues/2623 + // Also the correct hangup code as of VoIP v1 (with underscore) + // Also, if we don't have a reason + return ( +
    + { _t("This call has ended") } +
    + ); + } + + let reason; + if (hangupReason === "ice_failed") { + // We couldn't establish a connection at all + reason = _t("Could not connect media"); + } else if (hangupReason === "ice_timeout") { + // We established a connection but it died + reason = _t("Connection failed"); + } else if (hangupReason === "user_media_failed") { + // The other side couldn't open capture devices + reason = _t("Their device couldn't start the camera or microphone"); + } else if (hangupReason === "unknown_error") { + // An error code the other side doesn't have a way to express + // (as opposed to an error code they gave but we don't know about, + // in which case we show the error code) + reason = _t("An unknown error occurred"); + } else if (hangupReason === "invite_timeout") { + reason = _t("No answer"); + } else { + reason = _t('Unknown failure: %(reason)s)', {reason: hangupReason}); + } + + return ( +
    + + { _t("This call has failed") } +
    + ); + } + if (Array.from(TEXTUAL_STATES.keys()).includes(state)) { + return (
    { TEXTUAL_STATES.get(state) }
    ); - } else if (state === CustomCallState.Missed) { - content = ( + } + if (state === CustomCallState.Missed) { + return (
    { _t("You missed this call") }
    ); - } else { - content = ( -
    - { "The call is in an unknown state!" } -
    - ); } + // XXX: Should we translate this? + return ( +
    + { "The call is in an unknown state!" } +
    + ); + } + + render() { + const event = this.props.mxEvent; + const sender = event.sender ? event.sender.name : event.getSender(); + const callType = this.props.callEventGrouper.isVoice() ? _t("Voice call") : _t("Video call"); + const content = this.renderContent(this.state.callState); + return (
    @@ -119,7 +173,7 @@ export default class CallEvent extends React.Component { { sender }
    - { this.props.callEventGrouper.isVoice() ? _t("Voice call") : _t("Video call") } + { callType }
    From c03f0fb13df5a4e14c0801f69c70897bdca7114e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 14:47:46 +0200 Subject: [PATCH 036/148] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bcf1bdf6d7..573f22a7f3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1801,6 +1801,13 @@ "Compare emoji": "Compare emoji", "Connected": "Connected", "This call has ended": "This call has ended", + "Could not connect media": "Could not connect media", + "Connection failed": "Connection failed", + "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", + "An unknown error occurred": "An unknown error occurred", + "No answer": "No answer", + "Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)", + "This call has failed": "This call has failed", "You missed this call": "You missed this call", "Call back": "Call back", "Sunday": "Sunday", From 9db280bbe66c913541991f385c41f6b457bb70f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 15:31:46 +0200 Subject: [PATCH 037/148] Listen for CallsChanged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should avoid delays and such Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 15de2dcaf7..339b9359c2 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -18,7 +18,7 @@ limitations under the License. import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; -import CallHandler from '../../CallHandler'; +import CallHandler, { CallHandlerEvent } from '../../CallHandler'; import { EventEmitter } from 'events'; import { MatrixClientPeg } from "../../MatrixClientPeg"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -43,6 +43,12 @@ export default class CallEventGrouper extends EventEmitter { call: MatrixCall; state: CallState | CustomCallState; + constructor() { + super(); + + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall) + } + private get invite(): MatrixEvent { return this.events.find((event) => event.getType() === EventType.CallInvite); } @@ -95,10 +101,10 @@ export default class CallEventGrouper extends EventEmitter { private setCallListeners() { if (!this.call) return; - this.call.addListener(CallEvent.State, this.setCallState); + this.call.addListener(CallEvent.State, this.setState); } - private setCallState = () => { + private setState = () => { if (SUPPORTED_STATES.includes(this.call?.state)) { this.state = this.call.state; } else { @@ -113,13 +119,17 @@ export default class CallEventGrouper extends EventEmitter { this.emit(CallEventGrouperEvent.StateChanged, this.state); } - public add(event: MatrixEvent) { - const callId = event.getContent().call_id; - this.events.push(event); + private setCall = () => { + const callId = this.events[0].getContent().call_id; if (!this.call) { this.call = CallHandler.sharedInstance().getCallById(callId); this.setCallListeners(); } - this.setCallState(); + this.setState(); + } + + public add(event: MatrixEvent) { + this.events.push(event); + this.setState(); } } From 6b9e2042c37e3a8fce251586dcff71859ca057a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 16:28:57 +0200 Subject: [PATCH 038/148] Use a Set instead of an Array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 339b9359c2..267f8edacf 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -39,7 +39,7 @@ export enum CustomCallState { } export default class CallEventGrouper extends EventEmitter { - events: Array = []; + events: Set = new Set(); call: MatrixCall; state: CallState | CustomCallState; @@ -50,7 +50,7 @@ export default class CallEventGrouper extends EventEmitter { } private get invite(): MatrixEvent { - return this.events.find((event) => event.getType() === EventType.CallInvite); + return [...this.events].find((event) => event.getType() === EventType.CallInvite); } public answerCall = () => { @@ -65,7 +65,7 @@ export default class CallEventGrouper extends EventEmitter { defaultDispatcher.dispatch({ action: 'place_call', type: this.isVoice ? CallType.Voice : CallType.Video, - room_id: this.events[0]?.getRoomId(), + room_id: [...this.events][0]?.getRoomId(), }); } @@ -89,14 +89,14 @@ export default class CallEventGrouper extends EventEmitter { } public getHangupReason(): string | null { - return this.events.find((event) => event.getType() === EventType.CallHangup)?.getContent()?.reason; + return [...this.events].find((event) => event.getType() === EventType.CallHangup)?.getContent()?.reason; } /** * Returns true if there are only events from the other side - we missed the call */ private wasThisCallMissed(): boolean { - return !this.events.some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); + return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); } private setCallListeners() { @@ -108,7 +108,7 @@ export default class CallEventGrouper extends EventEmitter { if (SUPPORTED_STATES.includes(this.call?.state)) { this.state = this.call.state; } else { - const lastEvent = this.events[this.events.length - 1]; + const lastEvent = [...this.events][this.events.size - 1]; const lastEventType = lastEvent.getType(); if (this.wasThisCallMissed()) this.state = CustomCallState.Missed; @@ -120,7 +120,7 @@ export default class CallEventGrouper extends EventEmitter { } private setCall = () => { - const callId = this.events[0].getContent().call_id; + const callId = [...this.events][0].getContent().call_id; if (!this.call) { this.call = CallHandler.sharedInstance().getCallById(callId); this.setCallListeners(); @@ -129,7 +129,7 @@ export default class CallEventGrouper extends EventEmitter { } public add(event: MatrixEvent) { - this.events.push(event); + this.events.add(event); this.setState(); } } From 3bf28e3a6bd0fba78d956b1aff3e46cd2de2af5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Jun 2021 16:29:52 +0200 Subject: [PATCH 039/148] Remove Ended from SUPPORTED_STATES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 267f8edacf..4d32d48fb3 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -30,7 +30,6 @@ export enum CallEventGrouperEvent { const SUPPORTED_STATES = [ CallState.Connected, CallState.Connecting, - CallState.Ended, CallState.Ringing, ]; From 521b2445a8fe4c197a0655ec50bf08b0dd0e86dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 2 Jun 2021 10:18:32 +0200 Subject: [PATCH 040/148] Refactoring and fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 72 +++++++++---------- src/components/views/messages/CallEvent.tsx | 6 +- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 4d32d48fb3..8455eae0cd 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ - import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; @@ -38,9 +37,9 @@ export enum CustomCallState { } export default class CallEventGrouper extends EventEmitter { - events: Set = new Set(); - call: MatrixCall; - state: CallState | CustomCallState; + private events: Set = new Set(); + private call: MatrixCall; + public state: CallState | CustomCallState; constructor() { super(); @@ -52,6 +51,30 @@ export default class CallEventGrouper extends EventEmitter { return [...this.events].find((event) => event.getType() === EventType.CallInvite); } + private get hangup(): MatrixEvent { + return [...this.events].find((event) => event.getType() === EventType.CallHangup); + } + + public get isVoice(): boolean { + const invite = this.invite; + if (!invite) return; + + // FIXME: Find a better way to determine this from the event? + if (invite.getContent()?.offer?.sdp?.indexOf('m=video') !== -1) return false; + return true; + } + + public get hangupReason(): string | null { + return this.hangup?.getContent()?.reason; + } + + /** + * Returns true if there are only events from the other side - we missed the call + */ + private get callWasMissed(): boolean { + return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); + } + public answerCall = () => { this.call?.answer(); } @@ -68,35 +91,6 @@ export default class CallEventGrouper extends EventEmitter { }); } - public isVoice(): boolean { - const invite = this.invite; - - // FIXME: Find a better way to determine this from the event? - let isVoice = true; - if ( - invite.getContent().offer && invite.getContent().offer.sdp && - invite.getContent().offer.sdp.indexOf('m=video') !== -1 - ) { - isVoice = false; - } - - return isVoice; - } - - public getState(): CallState | CustomCallState { - return this.state; - } - - public getHangupReason(): string | null { - return [...this.events].find((event) => event.getType() === EventType.CallHangup)?.getContent()?.reason; - } - - /** - * Returns true if there are only events from the other side - we missed the call - */ - private wasThisCallMissed(): boolean { - return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); - } private setCallListeners() { if (!this.call) return; @@ -110,7 +104,7 @@ export default class CallEventGrouper extends EventEmitter { const lastEvent = [...this.events][this.events.size - 1]; const lastEventType = lastEvent.getType(); - if (this.wasThisCallMissed()) this.state = CustomCallState.Missed; + if (this.callWasMissed) this.state = CustomCallState.Missed; else if (lastEventType === EventType.CallHangup) this.state = CallState.Ended; else if (lastEventType === EventType.CallReject) this.state = CallState.Ended; else if (lastEventType === EventType.CallInvite && this.call) this.state = CallState.Connecting; @@ -119,16 +113,16 @@ export default class CallEventGrouper extends EventEmitter { } private setCall = () => { + if (this.call) return; + const callId = [...this.events][0].getContent().call_id; - if (!this.call) { - this.call = CallHandler.sharedInstance().getCallById(callId); - this.setCallListeners(); - } + this.call = CallHandler.sharedInstance().getCallById(callId); + this.setCallListeners(); this.setState(); } public add(event: MatrixEvent) { this.events.add(event); - this.setState(); + this.setCall(); } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index a4c0d02797..597c2feba8 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -43,7 +43,7 @@ export default class CallEvent extends React.Component { super(props); this.state = { - callState: this.props.callEventGrouper.getState(), + callState: this.props.callEventGrouper.state, } } @@ -77,7 +77,7 @@ export default class CallEvent extends React.Component { ); } if (state === CallState.Ended) { - const hangupReason = this.props.callEventGrouper.getHangupReason(); + const hangupReason = this.props.callEventGrouper.hangupReason; if (["user_hangup", "user hangup"].includes(hangupReason) || !hangupReason) { // workaround for https://github.com/vector-im/element-web/issues/5178 @@ -157,7 +157,7 @@ export default class CallEvent extends React.Component { render() { const event = this.props.mxEvent; const sender = event.sender ? event.sender.name : event.getSender(); - const callType = this.props.callEventGrouper.isVoice() ? _t("Voice call") : _t("Video call"); + const callType = this.props.callEventGrouper.isVoice ? _t("Voice call") : _t("Video call"); const content = this.renderContent(this.state.callState); return ( From 78229a2fd0af3e11e05abc59040334d6d5bd8920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 2 Jun 2021 10:20:11 +0200 Subject: [PATCH 041/148] Support user busy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/CallEvent.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 597c2feba8..e8e9afd2ee 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -110,6 +110,8 @@ export default class CallEvent extends React.Component { reason = _t("An unknown error occurred"); } else if (hangupReason === "invite_timeout") { reason = _t("No answer"); + } else if (hangupReason === "user_busy") { + reason = _t("The user you called is busy."); } else { reason = _t('Unknown failure: %(reason)s)', {reason: hangupReason}); } From b202f997e33acd017511477ea98cc504b9589ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 2 Jun 2021 10:30:17 +0200 Subject: [PATCH 042/148] Refactor setState() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 8455eae0cd..ab1444d4fa 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -55,6 +55,10 @@ export default class CallEventGrouper extends EventEmitter { return [...this.events].find((event) => event.getType() === EventType.CallHangup); } + private get reject(): MatrixEvent { + return [...this.events].find((event) => event.getType() === EventType.CallReject); + } + public get isVoice(): boolean { const invite = this.invite; if (!invite) return; @@ -101,13 +105,10 @@ export default class CallEventGrouper extends EventEmitter { if (SUPPORTED_STATES.includes(this.call?.state)) { this.state = this.call.state; } else { - const lastEvent = [...this.events][this.events.size - 1]; - const lastEventType = lastEvent.getType(); - if (this.callWasMissed) this.state = CustomCallState.Missed; - else if (lastEventType === EventType.CallHangup) this.state = CallState.Ended; - else if (lastEventType === EventType.CallReject) this.state = CallState.Ended; - else if (lastEventType === EventType.CallInvite && this.call) this.state = CallState.Connecting; + else if (this.reject) this.state = CallState.Ended; + else if (this.hangup) this.state = CallState.Ended; + else if (this.invite && this.call) this.state = CallState.Connecting; } this.emit(CallEventGrouperEvent.StateChanged, this.state); } From 3331e7bfbe8e5cf2b5b9b22779209636a23f6d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 2 Jun 2021 10:42:58 +0200 Subject: [PATCH 043/148] Use enums where possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/CallEvent.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index e8e9afd2ee..85b9e7f365 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -21,7 +21,7 @@ import { _t, _td } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper'; import FormButton from '../elements/FormButton'; -import { CallState } from 'matrix-js-sdk/src/webrtc/call'; +import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; interface IProps { @@ -79,7 +79,7 @@ export default class CallEvent extends React.Component { if (state === CallState.Ended) { const hangupReason = this.props.callEventGrouper.hangupReason; - if (["user_hangup", "user hangup"].includes(hangupReason) || !hangupReason) { + if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) { // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( @@ -94,13 +94,13 @@ export default class CallEvent extends React.Component { } let reason; - if (hangupReason === "ice_failed") { + if (hangupReason === CallErrorCode.IceFailed) { // We couldn't establish a connection at all reason = _t("Could not connect media"); } else if (hangupReason === "ice_timeout") { // We established a connection but it died reason = _t("Connection failed"); - } else if (hangupReason === "user_media_failed") { + } else if (hangupReason === CallErrorCode.NoUserMedia) { // The other side couldn't open capture devices reason = _t("Their device couldn't start the camera or microphone"); } else if (hangupReason === "unknown_error") { @@ -108,9 +108,9 @@ export default class CallEvent extends React.Component { // (as opposed to an error code they gave but we don't know about, // in which case we show the error code) reason = _t("An unknown error occurred"); - } else if (hangupReason === "invite_timeout") { + } else if (hangupReason === CallErrorCode.InviteTimeout) { reason = _t("No answer"); - } else if (hangupReason === "user_busy") { + } else if (hangupReason === CallErrorCode.UserBusy) { reason = _t("The user you called is busy."); } else { reason = _t('Unknown failure: %(reason)s)', {reason: hangupReason}); From e0572acb14adff8ca84cf0725279bc2ff92be4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 2 Jun 2021 19:22:22 +0200 Subject: [PATCH 044/148] Write tests for CallEventGrouper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../structures/CallEventGrouper-test.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 test/components/structures/CallEventGrouper-test.ts diff --git a/test/components/structures/CallEventGrouper-test.ts b/test/components/structures/CallEventGrouper-test.ts new file mode 100644 index 0000000000..98a5a16a22 --- /dev/null +++ b/test/components/structures/CallEventGrouper-test.ts @@ -0,0 +1,124 @@ +import "../../skinned-sdk"; +import { stubClient } from '../../test-utils'; +import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; +import { MatrixClient } from 'matrix-js-sdk'; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import CallEventGrouper, { CustomCallState } from "../../../src/components/structures/CallEventGrouper"; +import { CallState } from "matrix-js-sdk/src/webrtc/call"; + +const MY_USER_ID = "@me:here"; +const THEIR_USER_ID = "@they:here"; + +let client: MatrixClient; + +describe('CallEventGrouper', () => { + beforeEach(() => { + stubClient(); + client = MatrixClientPeg.get(); + client.getUserId = () => { + return MY_USER_ID; + }; + }); + + it("detects a missed call", () => { + const grouper = new CallEventGrouper(); + + grouper.add({ + getContent: () => { + return { + call_id: "callId", + }; + }, + getType: () => { + return EventType.CallInvite; + }, + sender: { + userId: THEIR_USER_ID, + }, + }); + + expect(grouper.state).toBe(CustomCallState.Missed); + }); + + it("detects an ended call", () => { + const grouperHangup = new CallEventGrouper(); + const grouperReject = new CallEventGrouper(); + + grouperHangup.add({ + getContent: () => { + return { + call_id: "callId", + }; + }, + getType: () => { + return EventType.CallInvite; + }, + sender: { + userId: MY_USER_ID, + }, + }); + grouperHangup.add({ + getContent: () => { + return { + call_id: "callId", + }; + }, + getType: () => { + return EventType.CallHangup; + }, + sender: { + userId: THEIR_USER_ID, + }, + }); + + grouperReject.add({ + getContent: () => { + return { + call_id: "callId", + }; + }, + getType: () => { + return EventType.CallInvite; + }, + sender: { + userId: MY_USER_ID, + }, + }); + grouperReject.add({ + getContent: () => { + return { + call_id: "callId", + }; + }, + getType: () => { + return EventType.CallReject; + }, + sender: { + userId: THEIR_USER_ID, + }, + }); + + expect(grouperHangup.state).toBe(CallState.Ended); + expect(grouperReject.state).toBe(CallState.Ended); + }); + + it("detects call type", () => { + const grouper = new CallEventGrouper(); + + grouper.add({ + getContent: () => { + return { + call_id: "callId", + offer: { + sdp: "this is definitely an SDP m=video", + }, + }; + }, + getType: () => { + return EventType.CallInvite; + }, + }); + + expect(grouper.isVoice).toBe(false); + }); +}); From 1c92e3168394280eaafddad84a242338ffdd0284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 2 Jun 2021 19:27:57 +0200 Subject: [PATCH 045/148] Add missing license header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../structures/CallEventGrouper-test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/components/structures/CallEventGrouper-test.ts b/test/components/structures/CallEventGrouper-test.ts index 98a5a16a22..5719d92902 100644 --- a/test/components/structures/CallEventGrouper-test.ts +++ b/test/components/structures/CallEventGrouper-test.ts @@ -1,3 +1,19 @@ +/* +Copyright 2021 Šimon Brandner + +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 "../../skinned-sdk"; import { stubClient } from '../../test-utils'; import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; From ae54a8f5469ba1a55d0e5642f5cc8b738ee794b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 4 Jun 2021 07:42:17 +0200 Subject: [PATCH 046/148] Return null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 15008a640a..9a1c416cdb 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -305,6 +305,7 @@ export default class CallHandler extends EventEmitter { for (const call of this.calls.values()) { if (call.callId === callId) return call; } + return null; } getCallForRoom(roomId: string): MatrixCall { From 3b2d6d442f96d80f4cb6f5de210e248545272eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 4 Jun 2021 07:43:30 +0200 Subject: [PATCH 047/148] Translate unknown call state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/CallEvent.tsx | 3 +-- src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 85b9e7f365..6139a2df6b 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -148,10 +148,9 @@ export default class CallEvent extends React.Component { ); } - // XXX: Should we translate this? return (
    - { "The call is in an unknown state!" } + { _t("The call is in an unknown state!") }
    ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 573f22a7f3..2db181285a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1810,6 +1810,7 @@ "This call has failed": "This call has failed", "You missed this call": "You missed this call", "Call back": "Call back", + "The call is in an unknown state!": "The call is in an unknown state!", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 22567a16efb28c43a4e97befbbb4a4ce8965723a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 9 Jun 2021 20:10:55 +0200 Subject: [PATCH 048/148] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bcd64b0ad7..8439afc57e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -534,8 +534,8 @@ "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.", "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.", "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.", - "Someone": "Someone", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.", + "Someone": "Someone", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s made future room history visible to all room members, from the point they joined.", From ae5cd9d7ac058f051454ace44b3d114908537e66 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 17 Jun 2021 14:11:44 +0100 Subject: [PATCH 049/148] Add new layout switcher UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Quirin Götz --- .../tabs/user/AppearanceUserSettingsTab.tsx | 110 ++++++++++++++++-- src/settings/Layout.ts | 4 +- src/settings/Settings.tsx | 8 ++ .../NewLayoutSwitcherController.ts | 26 +++++ 4 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 src/settings/controllers/NewLayoutSwitcherController.ts diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 9e27ed968e..bc31f750c3 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -37,6 +37,8 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { UIFeature } from "../../../../../settings/UIFeature"; import { Layout } from "../../../../../settings/Layout"; +import classNames from 'classnames'; +import StyledRadioButton from '../../../elements/StyledRadioButton'; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import { compare } from "../../../../../utils/strings"; @@ -235,6 +237,19 @@ export default class AppearanceUserSettingsTab extends React.Component): void => { + let layout; + switch (e.target.value) { + case "irc": layout = Layout.IRC; break; + case "group": layout = Layout.Group; break; + case "bubble": layout = Layout.Bubble; break; + } + + this.setState({ layout: layout }); + + SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout); + }; + private onIRCLayoutChange = (enabled: boolean) => { if (enabled) { this.setState({layout: Layout.IRC}); @@ -367,6 +382,77 @@ export default class AppearanceUserSettingsTab extends React.Component; } + private renderLayoutSection = () => { + return
    + { _t("Message layout") } + +
    +
    + + + { "IRC" } + +
    +
    +
    + + + {_t("Modern")} + +
    +
    +
    + + + {_t("Message bubbles")} + +
    +
    +
    ; + } + private renderAdvancedSection() { if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null; @@ -390,14 +476,17 @@ export default class AppearanceUserSettingsTab extends React.Component - this.onIRCLayoutChange(ev.target.checked)} - > - {_t("Enable experimental, compact IRC style layout")} - + + { !SettingsStore.getValue("feature_new_layout_switcher") ? + this.onIRCLayoutChange(ev.target.checked)} + > + {_t("Enable experimental, compact IRC style layout")} + : null + } {_t("Appearance Settings only affect this %(brand)s session.", { brand })}
    - {this.renderThemeSection()} - {this.renderFontSection()} - {this.renderAdvancedSection()} + { this.renderThemeSection() } + { SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null } + { this.renderFontSection() } + { this.renderAdvancedSection() }
    ); } diff --git a/src/settings/Layout.ts b/src/settings/Layout.ts index 3a42b2b510..d4e1f06c0a 100644 --- a/src/settings/Layout.ts +++ b/src/settings/Layout.ts @@ -1,5 +1,6 @@ /* Copyright 2021 Šimon Brandner +Copyright 2021 Quirin Götz Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,7 +20,8 @@ import PropTypes from 'prop-types'; /* TODO: This should be later reworked into something more generic */ export enum Layout { IRC = "irc", - Group = "group" + Group = "group", + Bubble = "bubble", } /* We need this because multiple components are still using JavaScript */ diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 155d039572..87edf886e0 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -41,6 +41,7 @@ import { Layout } from "./Layout"; import ReducedMotionController from './controllers/ReducedMotionController'; import IncompatibleController from "./controllers/IncompatibleController"; import SdkConfig from "../SdkConfig"; +import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -285,6 +286,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Show info about bridges in room settings"), default: false, }, + "feature_new_layout_switcher": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Explore new ways switching layouts (including a new bubble layout)"), + default: false, + controller: new NewLayoutSwitcherController(), + }, "RoomList.backgroundImage": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, diff --git a/src/settings/controllers/NewLayoutSwitcherController.ts b/src/settings/controllers/NewLayoutSwitcherController.ts new file mode 100644 index 0000000000..b1d6cac55e --- /dev/null +++ b/src/settings/controllers/NewLayoutSwitcherController.ts @@ -0,0 +1,26 @@ +/* +Copyright 2021 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 SettingController from "./SettingController"; +import { SettingLevel } from "../SettingLevel"; +import SettingsStore from "../SettingsStore"; +import { Layout } from "../Layout"; + +export default class NewLayoutSwitcherController extends SettingController { + public onChange(level: SettingLevel, roomId: string, newValue: any) { + // On disabling switch back to Layout.Group if Layout.Bubble + if (!newValue && SettingsStore.getValue("layout") == Layout.Bubble) { + SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + } + } +} From 7b6c3aec63e494e9a92bfaee6381ac3a04625154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 17 Jun 2021 16:33:00 +0200 Subject: [PATCH 050/148] Change some styling to better match the designs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 11 +++++++++-- src/components/views/messages/CallEvent.tsx | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 2e36daccfa..146bd0e883 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -21,7 +21,7 @@ limitations under the License. justify-content: space-between; background-color: $dark-panel-bg-color; - padding: 10px; + padding: 12px 16px 12px 12px; border-radius: 8px; margin: 10px auto; max-width: 75%; @@ -37,10 +37,17 @@ limitations under the License. flex-direction: column; margin-left: 10px; // To match mx_CallEvent + .mx_CallEvent_sender { + font-weight: 600; + font-size: 1.5rem; + line-height: 1.8rem; + } + .mx_CallEvent_type { font-weight: 400; color: gray; - line-height: $font-14px; + font-size: 1.2rem; + line-height: 1.5rem; } } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 6139a2df6b..cff7a46931 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -170,7 +170,7 @@ export default class CallEvent extends React.Component { height={32} />
    -
    +
    { sender }
    From 02e655933088e06eafc821f6f8e4964639b78aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 17 Jun 2021 17:04:56 +0200 Subject: [PATCH 051/148] Set text color to secondary-fg-color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 146bd0e883..5168514110 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -56,6 +56,7 @@ limitations under the License. display: flex; flex-direction: row; align-items: center; + color: $secondary-fg-color; .mx_CallEvent_content_callBack { margin-left: 10px; // To match mx_callEvent From 512c05465698f839b79da373effe86b56759638e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 17 Jun 2021 17:55:18 +0200 Subject: [PATCH 052/148] Add call type icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 28 ++++++++++++++++++++- src/components/views/messages/CallEvent.tsx | 10 +++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 5168514110..b4e2c15dbd 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -45,9 +45,35 @@ limitations under the License. .mx_CallEvent_type { font-weight: 400; - color: gray; + color: $secondary-fg-color; font-size: 1.2rem; line-height: 1.5rem; + display: flex; + align-items: center; + + .mx_CallEvent_type_icon { + height: 13px; + width: 13px; + margin-right: 5px; + + &::before { + content: ''; + position: absolute; + height: 13px; + width: 13px; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + } + + .mx_CallEvent_type_icon_voice::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + + .mx_CallEvent_type_icon_video::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } } } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index cff7a46931..00b62e4482 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -23,6 +23,7 @@ import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../ import FormButton from '../elements/FormButton'; import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; +import classNames from 'classnames'; interface IProps { mxEvent: MatrixEvent; @@ -158,8 +159,14 @@ export default class CallEvent extends React.Component { render() { const event = this.props.mxEvent; const sender = event.sender ? event.sender.name : event.getSender(); - const callType = this.props.callEventGrouper.isVoice ? _t("Voice call") : _t("Video call"); + const isVoice = this.props.callEventGrouper.isVoice; + const callType = isVoice ? _t("Voice call") : _t("Video call"); const content = this.renderContent(this.state.callState); + const callTypeIconClass = classNames({ + mx_CallEvent_type_icon: true, + mx_CallEvent_type_icon_voice: isVoice, + mx_CallEvent_type_icon_video: !isVoice, + }) return (
    @@ -174,6 +181,7 @@ export default class CallEvent extends React.Component { { sender }
    +
    { callType }
    From a781d6f1283f4558d8f65d1a776effbb41ae527d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 17 Jun 2021 18:13:52 +0200 Subject: [PATCH 053/148] Adjust padding and line-height a bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index b4e2c15dbd..d3405c7492 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -21,7 +21,7 @@ limitations under the License. justify-content: space-between; background-color: $dark-panel-bg-color; - padding: 12px 16px 12px 12px; + padding: 10px 16px 12px 10px; border-radius: 8px; margin: 10px auto; max-width: 75%; @@ -40,7 +40,7 @@ limitations under the License. .mx_CallEvent_sender { font-weight: 600; font-size: 1.5rem; - line-height: 1.8rem; + line-height: 1.9rem; } .mx_CallEvent_type { From 9b6195317ec1445c6d86e183409132d3cc88f36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 18 Jun 2021 16:14:54 +0200 Subject: [PATCH 054/148] Improve padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index d3405c7492..1597cf4b87 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -40,14 +40,15 @@ limitations under the License. .mx_CallEvent_sender { font-weight: 600; font-size: 1.5rem; - line-height: 1.9rem; + line-height: 1.8rem; + margin-bottom: 3px; } .mx_CallEvent_type { font-weight: 400; color: $secondary-fg-color; font-size: 1.2rem; - line-height: 1.5rem; + line-height: $font-13px; display: flex; align-items: center; From 62de75ab0077b55b526f0e5d6682aca4a7ef9ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 18 Jun 2021 16:19:57 +0200 Subject: [PATCH 055/148] Increase height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 1597cf4b87..1804462d4f 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -21,16 +21,17 @@ limitations under the License. justify-content: space-between; background-color: $dark-panel-bg-color; - padding: 10px 16px 12px 10px; border-radius: 8px; margin: 10px auto; max-width: 75%; box-sizing: border-box; + height: 60px; .mx_CallEvent_info { display: flex; flex-direction: row; align-items: center; + margin-left: 12px; .mx_CallEvent_info_basic { display: flex; @@ -84,6 +85,7 @@ limitations under the License. flex-direction: row; align-items: center; color: $secondary-fg-color; + margin-right: 16px; .mx_CallEvent_content_callBack { margin-left: 10px; // To match mx_callEvent From 707ecd8786da8897877c1a950492b41a931242b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 18 Jun 2021 17:03:48 +0200 Subject: [PATCH 056/148] Don't highlight bubble events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3af266caee..fdf933626f 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -130,6 +130,13 @@ $hover-select-border: 4px; .mx_EventTile_msgOption { grid-column: 2; } + + &:hover { + .mx_EventTile_line { + // To avoid bubble events being highlighted + background-color: inherit !important; + } + } } .mx_EventTile_reply { From 6271c5c3d8344e1c135f8f0736089e5d9945f39d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 18 Jun 2021 18:59:22 +0100 Subject: [PATCH 057/148] first iteration for message bubble layout --- res/css/_components.scss | 1 + res/css/views/messages/_MImageBody.scss | 1 - res/css/views/messages/_ReactionsRow.scss | 1 + res/css/views/rooms/_EventBubbleTile.scss | 149 ++++ res/css/views/rooms/_EventTile.scss | 769 +++++++++--------- .../views/elements/EventListSummary.tsx | 8 +- src/components/views/messages/UnknownBody.js | 3 +- src/components/views/rooms/EventTile.tsx | 17 +- 8 files changed, 559 insertions(+), 390 deletions(-) create mode 100644 res/css/views/rooms/_EventBubbleTile.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 56403ea190..67831e4a60 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -194,6 +194,7 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_EventBubbleTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 1c773c2f06..6ac6767ffa 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -16,7 +16,6 @@ limitations under the License. .mx_MImageBody { display: block; - margin-right: 34px; } .mx_MImageBody_thumbnail { diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index e05065eb02..b2bca6dfb3 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -26,6 +26,7 @@ limitations under the License. height: 24px; vertical-align: middle; margin-left: 4px; + margin-right: 4px; &::before { content: ''; diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss new file mode 100644 index 0000000000..28dce730ff --- /dev/null +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -0,0 +1,149 @@ +/* +Copyright 2021 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_EventTile[data-layout=bubble] { + + --avatarSize: 32px; + --gutterSize: 7px; + --cornerRadius: 5px; + + --maxWidth: 70%; + + position: relative; + margin-top: var(--gutterSize); + margin-left: var(--avatarSize); + margin-right: var(--avatarSize); + padding: 2px 0; + + &:hover { + background: rgb(242, 242, 242); + } + + .mx_SenderProfile, + .mx_EventTile_line { + width: fit-content; + max-width: 70%; + background: var(--backgroundColor); + } + + .mx_SenderProfile { + display: none; + padding: var(--gutterSize) var(--gutterSize) 0 var(--gutterSize); + border-top-left-radius: var(--cornerRadius); + border-top-right-radius: var(--cornerRadius); + } + + .mx_EventTile_line { + padding: var(--gutterSize); + border-radius: var(--cornerRadius); + } + + /* + .mx_SenderProfile + .mx_EventTile_line { + padding-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + */ + + .mx_EventTile_avatar { + position: absolute; + top: 0; + img { + border: 2px solid #fff; + border-radius: 50%; + } + } + + &[data-self=true] { + .mx_EventTile_line { + float: right; + } + .mx_ReactionsRow { + float: right; + clear: right; + display: flex; + + /* Moving the "add reaction button" before the reactions */ + > :last-child { + order: -1; + } + } + .mx_EventTile_avatar { + right: calc(-1 * var(--avatarSize)); + } + --backgroundColor: #F8FDFC; + } + + &:not([data-self=true]) { + .mx_EventTile_avatar { + left: calc(-1 * var(--avatarSize)); + } + --backgroundColor: #F7F8F9; + } + + &.mx_EventTile_bubbleContainer, + &.mx_EventTile_info, + & ~ .mx_EventListSummary[data-expanded=false] { + + --backgroundColor: transparent; + + display: flex; + align-items: center; + justify-content: center; + + .mx_EventTile_avatar { + position: static; + order: -1; + } + } + + & ~ .mx_EventListSummary { + --maxWidth: 95%; + .mx_EventListSummary_toggle { + float: none; + margin: 0; + order: 9; + } + } + + & + .mx_EventListSummary { + .mx_EventTile { + margin-top: 0; + padding: 0; + } + } + + .mx_EventListSummary_toggle { + margin-right: 55px; + } + + .mx_EventTile_line { + display: flex; + gap: var(--gutterSize); + > a { + order: 999; /* always display the timestamp as the last item */ + align-self: flex-end; + } + } + + .mx_EventTile_readAvatars { + position: absolute; + right: 0; + bottom: 0; + } + +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3af266caee..303118d57c 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 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. @@ -18,101 +18,382 @@ limitations under the License. $left-gutter: 64px; $hover-select-border: 4px; -.mx_EventTile { +.mx_EventTile:not([data-layout=bubble]) { max-width: 100%; clear: both; padding-top: 18px; font-size: $font-14px; position: relative; -} -.mx_EventTile.mx_EventTile_info { - padding-top: 1px; -} - -.mx_EventTile_avatar { - top: 14px; - left: 8px; - cursor: pointer; - user-select: none; -} - -.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { - top: $font-6px; - left: $left-gutter; -} - -.mx_EventTile_continuation { - padding-top: 0px !important; - - &.mx_EventTile_isEditing { - padding-top: 5px !important; - margin-top: -5px; + .mx_EventTile.mx_EventTile_info { + padding-top: 1px; } -} -.mx_EventTile_isEditing { - background-color: $header-panel-bg-color; -} + .mx_EventTile_avatar { + top: 14px; + left: 8px; + cursor: pointer; + user-select: none; + } -.mx_EventTile .mx_SenderProfile { - color: $primary-fg-color; - font-size: $font-14px; - display: inline-block; /* anti-zalgo, with overflow hidden */ - overflow: hidden; - cursor: pointer; - padding-bottom: 0px; - padding-top: 0px; - margin: 0px; - /* the next three lines, along with overflow hidden, truncate long display names */ - white-space: nowrap; - text-overflow: ellipsis; - max-width: calc(100% - $left-gutter); -} + .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { + top: $font-6px; + left: $left-gutter; + } -.mx_EventTile .mx_SenderProfile .mx_Flair { - opacity: 0.7; - margin-left: 5px; - display: inline-block; - vertical-align: top; - overflow: hidden; - user-select: none; + .mx_EventTile_continuation { + padding-top: 0px !important; - img { - vertical-align: -2px; - margin-right: 2px; + &.mx_EventTile_isEditing { + padding-top: 5px !important; + margin-top: -5px; + } + } + + .mx_EventTile_isEditing { + background-color: $header-panel-bg-color; + } + + .mx_EventTile .mx_SenderProfile { + color: $primary-fg-color; + font-size: $font-14px; + display: inline-block; /* anti-zalgo, with overflow hidden */ + overflow: hidden; + cursor: pointer; + padding-bottom: 0px; + padding-top: 0px; + margin: 0px; + /* the next three lines, along with overflow hidden, truncate long display names */ + white-space: nowrap; + text-overflow: ellipsis; + max-width: calc(100% - $left-gutter); + } + + .mx_EventTile .mx_SenderProfile .mx_Flair { + opacity: 0.7; + margin-left: 5px; + display: inline-block; + vertical-align: top; + overflow: hidden; + user-select: none; + + img { + vertical-align: -2px; + margin-right: 2px; + border-radius: 8px; + } + } + + .mx_EventTile_isEditing .mx_MessageTimestamp { + visibility: hidden; + } + + .mx_EventTile .mx_MessageTimestamp { + display: block; + white-space: nowrap; + left: 0px; + text-align: center; + user-select: none; + } + + .mx_EventTile_continuation .mx_EventTile_line { + clear: both; + } + + .mx_EventTile_line, .mx_EventTile_reply { + position: relative; + padding-left: $left-gutter; border-radius: 8px; } -} -.mx_EventTile_isEditing .mx_MessageTimestamp { - visibility: hidden; -} + .mx_RoomView_timeline_rr_enabled, + // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter + .mx_EventListSummary { + .mx_EventTile_line { + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + } + } -.mx_EventTile .mx_MessageTimestamp { - display: block; - white-space: nowrap; - left: 0px; - text-align: center; - user-select: none; -} + .mx_EventTile_reply { + margin-right: 10px; + } -.mx_EventTile_continuation .mx_EventTile_line { - clear: both; -} + .mx_EventTile_selected > div > a > .mx_MessageTimestamp { + left: calc(-$hover-select-border); + } -.mx_EventTile_line, .mx_EventTile_reply { - position: relative; - padding-left: $left-gutter; - border-radius: 8px; -} + .mx_EventTile:hover .mx_MessageActionBar, + .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, + [data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, + .mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { + visibility: visible; + } -.mx_RoomView_timeline_rr_enabled, -// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter -.mx_EventListSummary { - .mx_EventTile_line { - /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ - margin-right: 110px; + /* this is used for the tile for the event which is selected via the URL. + * TODO: ultimately we probably want some transition on here. + */ + .mx_EventTile_selected > .mx_EventTile_line { + border-left: $accent-color 4px solid; + padding-left: calc($left-gutter - $hover-select-border); + background-color: $event-selected-color; + } + + .mx_EventTile_highlight, + .mx_EventTile_highlight .markdown-body { + color: $event-highlight-fg-color; + + .mx_EventTile_line { + background-color: $event-highlight-bg-color; + } + } + + .mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } + + .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px - $hover-select-border); + } + + .mx_EventTile:hover .mx_EventTile_line, + .mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, + .mx_EventTile.focus-visible:focus-within .mx_EventTile_line { + background-color: $event-selected-color; + } + + .mx_EventTile_searchHighlight { + background-color: $accent-color; + color: $accent-fg-color; + border-radius: 5px; + padding-left: 2px; + padding-right: 2px; + cursor: pointer; + } + + .mx_EventTile_searchHighlight a { + background-color: $accent-color; + color: $accent-fg-color; + } + + .mx_EventTile_receiptSent, + .mx_EventTile_receiptSending { + // We don't use `position: relative` on the element because then it won't line + // up with the other read receipts + + &::before { + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 14px; + width: 14px; + height: 14px; + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + } + } + .mx_EventTile_receiptSent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + } + .mx_EventTile_receiptSending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + } + + .mx_EventTile_contextual { + opacity: 0.4; + } + + .mx_EventTile_msgOption { + float: right; + text-align: right; + position: relative; + width: 90px; + + /* Hack to stop the height of this pushing the messages apart. + Replaces margin-top: -6px. This interacts better with a read + marker being in between. Content overflows. */ + height: 1px; + + margin-right: 10px; + } + + .mx_EventTile_msgOption a { + text-decoration: none; + } + + /* all the overflow-y: hidden; are to trap Zalgos - + but they introduce an implicit overflow-x: auto. + so make that explicitly hidden too to avoid random + horizontal scrollbars occasionally appearing, like in + https://github.com/vector-im/vector-web/issues/1154 + */ + .mx_EventTile_content { + display: block; + overflow-y: hidden; + overflow-x: hidden; + margin-right: 34px; + } + + /* De-zalgoing */ + .mx_EventTile_body { + overflow-y: hidden; + } + + /* Spoiler stuff */ + .mx_EventTile_spoiler { + cursor: pointer; + } + + .mx_EventTile_spoiler_reason { + color: $event-timestamp-color; + font-size: $font-11px; + } + + .mx_EventTile_spoiler_content { + filter: blur(5px) saturate(0.1) sepia(1); + transition-duration: 0.5s; + } + + .mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { + filter: none; + } + + .mx_EventTile_e2eIcon { + position: absolute; + top: 6px; + left: 44px; + width: 14px; + height: 14px; + display: block; + bottom: 0; + right: 0; + opacity: 0.2; + background-repeat: no-repeat; + background-size: contain; + + &::before, &::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 90%; + } + } + + .mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; + } + + .mx_EventTile_e2eIcon_unknown { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; + } + + .mx_EventTile_e2eIcon_unencrypted { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; + } + + .mx_EventTile_e2eIcon_unauthenticated { + &::after { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; + } + opacity: 1; + } + + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + padding-left: calc($left-gutter - $hover-select-border); + } + + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { + border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; + } + + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { + border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; + } + + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; + } + + .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px - $hover-select-border); + } + + /* End to end encryption stuff */ + .mx_EventTile:hover .mx_EventTile_e2eIcon { + opacity: 1; + } + + // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { + left: calc(-$hover-select-border); + } + + // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { + display: block; + left: 41px; + } + + .mx_EventTile_tileError { + color: red; + text-align: center; + + // Remove some of the default tile padding so that the error is centered + margin-right: 0; + .mx_EventTile_line { + padding-left: 0; + margin-right: 0; + } + + .mx_EventTile_line span { + padding: 4px 8px; + } + + a { + margin-left: 1em; + } + } + + .mx_MImageBody { + margin-right: 34px; } } @@ -132,121 +413,6 @@ $hover-select-border: 4px; } } -.mx_EventTile_reply { - margin-right: 10px; -} - -/* HACK to override line-height which is already marked important elsewhere */ -.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { - font-size: 48px !important; - line-height: 57px !important; -} - -.mx_EventTile_selected > div > a > .mx_MessageTimestamp { - left: calc(-$hover-select-border); -} - -.mx_EventTile:hover .mx_MessageActionBar, -.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, -[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, -.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { - visibility: visible; -} - -/* this is used for the tile for the event which is selected via the URL. - * TODO: ultimately we probably want some transition on here. - */ -.mx_EventTile_selected > .mx_EventTile_line { - border-left: $accent-color 4px solid; - padding-left: calc($left-gutter - $hover-select-border); - background-color: $event-selected-color; -} - -.mx_EventTile_highlight, -.mx_EventTile_highlight .markdown-body { - color: $event-highlight-fg-color; - - .mx_EventTile_line { - background-color: $event-highlight-bg-color; - } -} - -.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); -} - -.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px - $hover-select-border); -} - -.mx_EventTile:hover .mx_EventTile_line, -.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, -.mx_EventTile.focus-visible:focus-within .mx_EventTile_line { - background-color: $event-selected-color; -} - -.mx_EventTile_searchHighlight { - background-color: $accent-color; - color: $accent-fg-color; - border-radius: 5px; - padding-left: 2px; - padding-right: 2px; - cursor: pointer; -} - -.mx_EventTile_searchHighlight a { - background-color: $accent-color; - color: $accent-fg-color; -} - -.mx_EventTile_receiptSent, -.mx_EventTile_receiptSending { - // We don't use `position: relative` on the element because then it won't line - // up with the other read receipts - - &::before { - background-color: $tertiary-fg-color; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 14px; - width: 14px; - height: 14px; - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - } -} -.mx_EventTile_receiptSent::before { - mask-image: url('$(res)/img/element-icons/circle-sent.svg'); -} -.mx_EventTile_receiptSending::before { - mask-image: url('$(res)/img/element-icons/circle-sending.svg'); -} - -.mx_EventTile_contextual { - opacity: 0.4; -} - -.mx_EventTile_msgOption { - float: right; - text-align: right; - position: relative; - width: 90px; - - /* Hack to stop the height of this pushing the messages apart. - Replaces margin-top: -6px. This interacts better with a read - marker being in between. Content overflows. */ - height: 1px; - - margin-right: 10px; -} - -.mx_EventTile_msgOption a { - text-decoration: none; -} - .mx_EventTile_readAvatars { position: relative; display: inline-block; @@ -277,180 +443,10 @@ $hover-select-border: 4px; position: absolute; } -/* all the overflow-y: hidden; are to trap Zalgos - - but they introduce an implicit overflow-x: auto. - so make that explicitly hidden too to avoid random - horizontal scrollbars occasionally appearing, like in - https://github.com/vector-im/vector-web/issues/1154 - */ -.mx_EventTile_content { - display: block; - overflow-y: hidden; - overflow-x: hidden; - margin-right: 34px; -} - -/* De-zalgoing */ -.mx_EventTile_body { - overflow-y: hidden; -} - -/* Spoiler stuff */ -.mx_EventTile_spoiler { - cursor: pointer; -} - -.mx_EventTile_spoiler_reason { - color: $event-timestamp-color; - font-size: $font-11px; -} - -.mx_EventTile_spoiler_content { - filter: blur(5px) saturate(0.1) sepia(1); - transition-duration: 0.5s; -} - -.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { - filter: none; -} - -.mx_EventTile_e2eIcon { - position: absolute; - top: 6px; - left: 44px; - width: 14px; - height: 14px; - display: block; - bottom: 0; - right: 0; - opacity: 0.2; - background-repeat: no-repeat; - background-size: contain; - - &::before, &::after { - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - } - - &::before { - background-color: #ffffff; - mask-image: url('$(res)/img/e2e/normal.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 90%; - } -} - -.mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; -} - -.mx_EventTile_e2eIcon_unknown { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; -} - -.mx_EventTile_e2eIcon_unencrypted { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; -} - -.mx_EventTile_e2eIcon_unauthenticated { - &::after { - mask-image: url('$(res)/img/e2e/normal.svg'); - background-color: $composer-e2e-icon-color; - } - opacity: 1; -} - -.mx_EventTile_keyRequestInfo { - font-size: $font-12px; -} - -.mx_EventTile_keyRequestInfo_text { - opacity: 0.5; -} - -.mx_EventTile_keyRequestInfo_text a { - color: $primary-fg-color; - text-decoration: underline; - cursor: pointer; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p { - text-align: auto; - margin-left: 3px; - margin-right: 3px; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child { - margin-top: 0px; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child { - margin-bottom: 0px; -} - -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - padding-left: calc($left-gutter - $hover-select-border); -} - -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { - border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { - border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px - $hover-select-border); -} - -/* End to end encryption stuff */ -.mx_EventTile:hover .mx_EventTile_e2eIcon { - opacity: 1; -} - -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { - left: calc(-$hover-select-border); -} - -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { - display: block; - left: 41px; +/* HACK to override line-height which is already marked important elsewhere */ +.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { + font-size: 48px !important; + line-height: 57px !important; } .mx_EventTile_content .mx_EventTile_edited { @@ -601,24 +597,33 @@ $hover-select-border: 4px; /* end of overrides */ -.mx_EventTile_tileError { - color: red; - text-align: center; - // Remove some of the default tile padding so that the error is centered - margin-right: 0; - .mx_EventTile_line { - padding-left: 0; - margin-right: 0; - } +.mx_EventTile_keyRequestInfo { + font-size: $font-12px; +} - .mx_EventTile_line span { - padding: 4px 8px; - } +.mx_EventTile_keyRequestInfo_text { + opacity: 0.5; +} - a { - margin-left: 1em; - } +.mx_EventTile_keyRequestInfo_text a { + color: $primary-fg-color; + text-decoration: underline; + cursor: pointer; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p { + text-align: auto; + margin-left: 3px; + margin-right: 3px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child { + margin-top: 0px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child { + margin-bottom: 0px; } @media only screen and (max-width: 480px) { diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 86d3e082ad..3c64337367 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -63,7 +63,7 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
  • +
  • { children }
  • ); @@ -92,7 +92,7 @@ const EventListSummary: React.FC = ({ } return ( -
  • +
  • { expanded ? _t('collapse') : _t('expand') } @@ -101,4 +101,8 @@ const EventListSummary: React.FC = ({ ); }; +EventListSummary.defaultProps = { + startExpanded: false, +}; + export default EventListSummary; diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 786facc340..fdf0387a69 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -17,11 +17,12 @@ limitations under the License. import React, {forwardRef} from "react"; -export default forwardRef(({mxEvent}, ref) => { +export default forwardRef(({mxEvent, children}, ref) => { const text = mxEvent.getContent().body; return ( { text } + { children } ); }); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 85b9cac2c4..a76cc04660 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -29,7 +29,7 @@ import { hasText } from "../../../TextForEvent"; import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; -import {Layout} from "../../../settings/Layout"; +import { Layout } from "../../../settings/Layout"; import {formatTime} from "../../../DateUtils"; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; @@ -988,8 +988,13 @@ export default class EventTile extends React.Component { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const showTimestamp = this.props.mxEvent.getTs() && - (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); + const showTimestamp = this.props.mxEvent.getTs() + && (this.props.alwaysShowTimestamps + || this.props.last + || this.state.hover + || this.state.actionBarFocused) + || this.props.layout === Layout.Bubble; + const timestamp = showTimestamp ? : null; @@ -1168,6 +1173,8 @@ export default class EventTile extends React.Component { this.props.alwaysShowTimestamps || this.state.hover, ); + const isOwnEvent = this.props.mxEvent.sender.userId === MatrixClientPeg.get().getUserId(); + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( React.createElement(this.props.as || "li", { @@ -1177,6 +1184,8 @@ export default class EventTile extends React.Component { "aria-live": ariaLive, "aria-atomic": "true", "data-scroll-tokens": scrollToken, + "data-layout": this.props.layout, + "data-self": isOwnEvent, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), }, [ @@ -1198,9 +1207,9 @@ export default class EventTile extends React.Component { onHeightChanged={this.props.onHeightChanged} /> { keyRequestInfo } - { reactionsRow } { actionBar }
  • , + reactionsRow, msgOption, avatar, From ccfc7fe42119eb9986ab01d3d3efa69ea0297888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Jun 2021 19:30:19 +0200 Subject: [PATCH 058/148] Make call silencing more flexible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 36 ++++++++++++++++++- src/components/views/voip/IncomingCallBox.tsx | 20 ++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 2f508191d6..131b2ac579 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3; // (and store the ID of their native room) export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; -export enum AudioID { +enum AudioID { Ring = 'ringAudio', Ringback = 'ringbackAudio', CallEnd = 'callendAudio', @@ -142,6 +142,7 @@ export enum PlaceCallType { export enum CallHandlerEvent { CallsChanged = "calls_changed", CallChangeRoom = "call_change_room", + SilencedCallsChanged = "silenced_calls_changed", } export default class CallHandler extends EventEmitter { @@ -164,6 +165,8 @@ export default class CallHandler extends EventEmitter { // do the async lookup when we get new information and then store these mappings here private assertedIdentityNativeUsers = new Map(); + private silencedCalls = new Map(); // callId -> silenced + static sharedInstance() { if (!window.mxCallHandler) { window.mxCallHandler = new CallHandler() @@ -224,6 +227,33 @@ export default class CallHandler extends EventEmitter { } } + public silenceCall(callId: string) { + this.silencedCalls.set(callId, true); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); + + // Don't pause audio if we have calls which are still ringing + if (this.areAnyCallsUnsilenced()) return; + this.pause(AudioID.Ring); + } + + public unSilenceCall(callId: string) { + this.silencedCalls.set(callId, false); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); + this.play(AudioID.Ring); + } + + public isCallSilenced(callId: string): boolean { + return this.silencedCalls.get(callId); + } + + /** + * Returns true if there is at least one unsilenced call + * @returns {boolean} + */ + private areAnyCallsUnsilenced(): boolean { + return [...this.silencedCalls.values()].includes(false); + } + private async checkProtocols(maxTries) { try { const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); @@ -616,6 +646,8 @@ export default class CallHandler extends EventEmitter { private removeCallForRoom(roomId: string) { console.log("Removing call for room ", roomId); + this.silencedCalls.delete(this.calls.get(roomId).callId); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.calls.delete(roomId); this.emit(CallHandlerEvent.CallsChanged, this.calls); } @@ -825,6 +857,8 @@ export default class CallHandler extends EventEmitter { console.log("Adding call for room ", mappedRoomId); this.calls.set(mappedRoomId, call) this.emit(CallHandlerEvent.CallsChanged, this.calls); + this.silencedCalls.set(call.callId, false); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.setCallListeners(call); // get ready to send encrypted events in the room, so if the user does answer diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index a0660318bc..cce4687f90 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -21,7 +21,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import { ActionPayload } from '../../../dispatcher/payloads'; -import CallHandler, { AudioID } from '../../../CallHandler'; +import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; import RoomAvatar from '../avatars/RoomAvatar'; import FormButton from '../elements/FormButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; @@ -51,8 +51,13 @@ export default class IncomingCallBox extends React.Component { }; } + componentDidMount = () => { + CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + } + public componentWillUnmount() { dis.unregister(this.dispatcherRef); + CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); } private onAction = (payload: ActionPayload) => { @@ -73,6 +78,12 @@ export default class IncomingCallBox extends React.Component { } }; + private onSilencedCallsChanged = () => { + const callId = this.state.incomingCall?.callId; + if (!callId) return; + this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) }); + } + private onAnswerClick: React.MouseEventHandler = (e) => { e.stopPropagation(); dis.dispatch({ @@ -91,9 +102,10 @@ export default class IncomingCallBox extends React.Component { private onSilenceClick: React.MouseEventHandler = (e) => { e.stopPropagation(); - const newState = !this.state.silenced - this.setState({silenced: newState}); - newState ? CallHandler.sharedInstance().pause(AudioID.Ring) : CallHandler.sharedInstance().play(AudioID.Ring); + const callId = this.state.incomingCall.callId; + this.state.silenced ? + CallHandler.sharedInstance().unSilenceCall(callId): + CallHandler.sharedInstance().silenceCall(callId); } public render() { From 401fe1d05bc8ab9f9086bbcd8e5e1daa6ca469da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Jun 2021 20:02:51 +0200 Subject: [PATCH 059/148] Add call silencing to CallEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 24 +++++++++++++++++++ src/components/structures/CallEventGrouper.ts | 22 ++++++++++++++--- src/components/views/messages/CallEvent.tsx | 22 ++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 1804462d4f..1bf62af22e 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -94,5 +94,29 @@ limitations under the License. .mx_CallEvent_content_tooltip { margin-right: 5px; } + + .mx_CallEvent_iconButton { + display: inline-flex; + margin-right: 16px; + + &::before { + content: ''; + + height: 16px; + width: 16px; + background-color: $icon-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_CallEvent_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_CallEvent_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } } } diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index ab1444d4fa..c71d1a032a 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -24,6 +24,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; export enum CallEventGrouperEvent { StateChanged = "state_changed", + SilencedChanged = "silenced_changed", } const SUPPORTED_STATES = [ @@ -44,7 +45,8 @@ export default class CallEventGrouper extends EventEmitter { constructor() { super(); - CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall) + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall); + CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); } private get invite(): MatrixEvent { @@ -79,6 +81,15 @@ export default class CallEventGrouper extends EventEmitter { return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); } + private get callId(): string { + return [...this.events][0].getContent().call_id; + } + + private onSilencedCallsChanged = () => { + const newState = CallHandler.sharedInstance().isCallSilenced(this.callId); + this.emit(CallEventGrouperEvent.SilencedChanged, newState) + } + public answerCall = () => { this.call?.answer(); } @@ -95,6 +106,12 @@ export default class CallEventGrouper extends EventEmitter { }); } + public toggleSilenced = () => { + const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId); + silenced ? + CallHandler.sharedInstance().unSilenceCall(this.callId) : + CallHandler.sharedInstance().silenceCall(this.callId); + } private setCallListeners() { if (!this.call) return; @@ -116,8 +133,7 @@ export default class CallEventGrouper extends EventEmitter { private setCall = () => { if (this.call) return; - const callId = [...this.events][0].getContent().call_id; - this.call = CallHandler.sharedInstance().getCallById(callId); + this.call = CallHandler.sharedInstance().getCallById(this.callId); this.setCallListeners(); this.setState(); } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 00b62e4482..4710391050 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -24,6 +24,7 @@ import FormButton from '../elements/FormButton'; import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import classNames from 'classnames'; +import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; interface IProps { mxEvent: MatrixEvent; @@ -32,6 +33,7 @@ interface IProps { interface IState { callState: CallState | CustomCallState; + silenced: boolean; } const TEXTUAL_STATES: Map = new Map([ @@ -45,25 +47,43 @@ export default class CallEvent extends React.Component { this.state = { callState: this.props.callEventGrouper.state, + silenced: false, } } componentDidMount() { this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); + this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged); } componentWillUnmount() { this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); + this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged); } + private onSilencedChanged = (newState) => { + this.setState({ silenced: newState }); + }; + private onStateChanged = (newState: CallState) => { this.setState({callState: newState}); - } + }; private renderContent(state: CallState | CustomCallState): JSX.Element { if (state === CallState.Ringing) { + const silenceClass = classNames({ + "mx_CallEvent_iconButton": true, + "mx_CallEvent_unSilence": this.state.silenced, + "mx_CallEvent_silence": !this.state.silenced, + }); + return (
    + Date: Mon, 21 Jun 2021 16:49:10 +0200 Subject: [PATCH 060/148] Migrate from FormButton to AccessibleButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/CallEvent.tsx | 23 ++++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 4710391050..a6263e408f 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t, _td } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper'; -import FormButton from '../elements/FormButton'; +import AccessibleButton from '../elements/AccessibleButton'; import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import classNames from 'classnames'; @@ -84,16 +84,18 @@ export default class CallEvent extends React.Component { onClick={this.props.callEventGrouper.toggleSilenced} title={this.state.silenced ? _t("Sound on"): _t("Silence call")} /> - - + { _t("Decline") } + + + > + { _t("Accept") } +
    ); } @@ -159,12 +161,13 @@ export default class CallEvent extends React.Component { return (
    { _t("You missed this call") } - + > + { _t("Call back") } +
    ); } From 202cb0f5d81b971148b2375af32424d853940c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 17:05:36 +0200 Subject: [PATCH 061/148] Fix styling of buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 7 ++++++- src/components/views/messages/CallEvent.tsx | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 1bf62af22e..d83dfb39ad 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -87,7 +87,12 @@ limitations under the License. color: $secondary-fg-color; margin-right: 16px; - .mx_CallEvent_content_callBack { + .mx_CallEvent_content_button { + height: 24px; + padding: 0px 12px; + } + + .mx_CallEvent_content_button_callBack { margin-left: 10px; // To match mx_callEvent } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index a6263e408f..bb219c458d 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -85,12 +85,14 @@ export default class CallEvent extends React.Component { title={this.state.silenced ? _t("Sound on"): _t("Silence call")} /> { _t("Decline") } @@ -162,7 +164,7 @@ export default class CallEvent extends React.Component {
    { _t("You missed this call") } From 6b9dfa37c5170ed4229eaf382b4bea1499d37f53 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 30 Jun 2021 09:00:14 +0100 Subject: [PATCH 062/148] Migrate UnknownBody to TypeScript --- .../views/messages/{UnknownBody.js => UnknownBody.tsx} | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) rename src/components/views/messages/{UnknownBody.js => UnknownBody.tsx} (78%) diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.tsx similarity index 78% rename from src/components/views/messages/UnknownBody.js rename to src/components/views/messages/UnknownBody.tsx index 78a1846b68..b09afa54e9 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.tsx @@ -16,8 +16,14 @@ limitations under the License. */ import React, { forwardRef } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src"; -export default forwardRef(({ mxEvent, children }, ref) => { +interface IProps { + mxEvent: MatrixEvent; + children?: React.ReactNode; +} + +export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject) => { const text = mxEvent.getContent().body; return ( From d1c6cfe6b95c903c517cc52c31664a979d70153b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 30 Jun 2021 12:06:16 +0100 Subject: [PATCH 063/148] Improved message bubble layout (no reply) --- res/css/views/avatars/_BaseAvatar.scss | 1 + res/css/views/rooms/_EventBubbleTile.scss | 51 +++++++++++++++++------ res/css/views/rooms/_EventTile.scss | 40 +++++++++--------- src/components/views/rooms/EventTile.tsx | 7 +++- 4 files changed, 64 insertions(+), 35 deletions(-) diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index cbddd97e18..65e4493f19 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -27,6 +27,7 @@ limitations under the License. // https://bugzilla.mozilla.org/show_bug.cgi?id=255139 display: inline-block; user-select: none; + line-height: 1; } .mx_BaseAvatar_initial { diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 28dce730ff..2009e7dcd8 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -36,34 +36,24 @@ limitations under the License. .mx_EventTile_line { width: fit-content; max-width: 70%; - background: var(--backgroundColor); } .mx_SenderProfile { - display: none; padding: var(--gutterSize) var(--gutterSize) 0 var(--gutterSize); - border-top-left-radius: var(--cornerRadius); - border-top-right-radius: var(--cornerRadius); } .mx_EventTile_line { padding: var(--gutterSize); border-radius: var(--cornerRadius); + background: var(--backgroundColor); } - /* - .mx_SenderProfile + .mx_EventTile_line { - padding-top: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; - } - */ - .mx_EventTile_avatar { position: absolute; top: 0; + line-height: 1; img { - border: 2px solid #fff; + box-shadow: 0 0 0 2px #fff; border-radius: 50%; } } @@ -72,6 +62,9 @@ limitations under the License. .mx_EventTile_line { float: right; } + .mx_SenderProfile { + display: none; + } .mx_ReactionsRow { float: right; clear: right; @@ -88,6 +81,22 @@ limitations under the License. --backgroundColor: #F8FDFC; } + &[data-has-reply=true] { + > .mx_EventTile_line { + flex-direction: column; + + > a { + margin-top: -12px; + } + } + + .mx_ReplyThread_show { + order: 99999; + background: white; + box-shadow: 0 0 0 var(--gutterSize) white; + } + } + &:not([data-self=true]) { .mx_EventTile_avatar { left: calc(-1 * var(--avatarSize)); @@ -100,6 +109,7 @@ limitations under the License. & ~ .mx_EventListSummary[data-expanded=false] { --backgroundColor: transparent; + --gutterSize: 0; display: flex; align-items: center; @@ -140,10 +150,25 @@ limitations under the License. } } + /* Special layout scenario for "Unable To Decrypt (UTD)" events */ + &.mx_EventTile_bad > .mx_EventTile_line { + flex-direction: column; + > a { + position: absolute; + bottom: var(--gutterSize); + } + } + + .mx_EventTile_readAvatars { position: absolute; right: 0; bottom: 0; } + .mx_MTextBody { + /* 30px equates to the width of the timestamp */ + max-width: calc(100% - 35px - var(--gutterSize)); + } + } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1052b87b0d..11b9f5e959 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -372,26 +372,6 @@ $hover-select-border: 4px; left: 41px; } - .mx_EventTile_tileError { - color: red; - text-align: center; - - // Remove some of the default tile padding so that the error is centered - margin-right: 0; - .mx_EventTile_line { - padding-left: 0; - margin-right: 0; - } - - .mx_EventTile_line span { - padding: 4px 8px; - } - - a { - margin-left: 1em; - } - } - .mx_MImageBody { margin-right: 34px; } @@ -626,6 +606,26 @@ $hover-select-border: 4px; margin-bottom: 0px; } +.mx_EventTile_tileError { + color: red; + text-align: center; + + // Remove some of the default tile padding so that the error is centered + margin-right: 0; + .mx_EventTile_line { + padding-left: 0; + margin-right: 0; + } + + .mx_EventTile_line span { + padding: 4px 8px; + } + + a { + margin-left: 1em; + } +} + @media only screen and (max-width: 480px) { .mx_EventTile_line, .mx_EventTile_reply { padding-left: 0; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 6a8748883b..6040e1962f 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -163,8 +163,6 @@ export function getHandlerTile(ev) { return eventTileTypes[type]; } -const MAX_READ_AVATARS = 5; - // Our component structure for EventTiles on the timeline is: // // .-EventTile------------------------------------------------. @@ -649,6 +647,10 @@ export default class EventTile extends React.Component { return ; } + const MAX_READ_AVATARS = this.props.layout == Layout.Bubble + ? 2 + : 5; + // return early if there are no read receipts if (!this.props.readReceipts || this.props.readReceipts.length === 0) { // We currently must include `mx_EventTile_readAvatars` in the DOM @@ -1194,6 +1196,7 @@ export default class EventTile extends React.Component { "data-scroll-tokens": scrollToken, "data-layout": this.props.layout, "data-self": isOwnEvent, + "data-has-reply": !!thread, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), }, [ From 209344d443853f345552b62eb734dca862012259 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 30 Jun 2021 17:04:07 +0100 Subject: [PATCH 064/148] improvements to bubble layout --- res/css/views/rooms/_EventBubbleTile.scss | 60 +++++++++++++---------- res/css/views/rooms/_EventTile.scss | 14 +++--- src/components/views/rooms/EventTile.tsx | 3 +- src/i18n/strings/en_EN.json | 4 ++ 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 2009e7dcd8..284f9bb70f 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -15,17 +15,15 @@ limitations under the License. */ .mx_EventTile[data-layout=bubble] { - --avatarSize: 32px; --gutterSize: 7px; - --cornerRadius: 5px; - + --cornerRadius: 12px; --maxWidth: 70%; position: relative; margin-top: var(--gutterSize); - margin-left: var(--avatarSize); - margin-right: var(--avatarSize); + margin-left: calc(var(--avatarSize) + var(--gutterSize)); + margin-right: calc(var(--gutterSize) + var(--avatarSize)); padding: 2px 0; &:hover { @@ -46,6 +44,12 @@ limitations under the License. padding: var(--gutterSize); border-radius: var(--cornerRadius); background: var(--backgroundColor); + display: flex; + gap: var(--gutterSize); + > a { + position: absolute; + left: -33px; + } } .mx_EventTile_avatar { @@ -78,16 +82,13 @@ limitations under the License. .mx_EventTile_avatar { right: calc(-1 * var(--avatarSize)); } + --backgroundColor: #F8FDFC; } &[data-has-reply=true] { > .mx_EventTile_line { flex-direction: column; - - > a { - margin-top: -12px; - } } .mx_ReplyThread_show { @@ -95,19 +96,41 @@ limitations under the License. background: white; box-shadow: 0 0 0 var(--gutterSize) white; } + + .mx_ReplyThread { + margin: 0 calc(-1 * var(--gutterSize)); + + .mx_EventTile_reply { + padding: 0; + > a { + display: none !important; + } + } + + .mx_EventTile { + display: flex; + gap: var(--gutterSize); + .mx_EventTile_avatar { + position: static; + } + .mx_SenderProfile { + display: none; + } + } + } } &:not([data-self=true]) { .mx_EventTile_avatar { left: calc(-1 * var(--avatarSize)); } + --backgroundColor: #F7F8F9; } &.mx_EventTile_bubbleContainer, &.mx_EventTile_info, & ~ .mx_EventListSummary[data-expanded=false] { - --backgroundColor: transparent; --gutterSize: 0; @@ -141,34 +164,21 @@ limitations under the License. margin-right: 55px; } - .mx_EventTile_line { - display: flex; - gap: var(--gutterSize); - > a { - order: 999; /* always display the timestamp as the last item */ - align-self: flex-end; - } - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { flex-direction: column; - > a { - position: absolute; - bottom: var(--gutterSize); - } } .mx_EventTile_readAvatars { position: absolute; - right: 0; + right: -45px; bottom: 0; + top: auto; } .mx_MTextBody { /* 30px equates to the width of the timestamp */ max-width: calc(100% - 35px - var(--gutterSize)); } - } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 11b9f5e959..446c524e81 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -287,14 +287,14 @@ $hover-select-border: 4px; mask-size: contain; } - &::before { - background-color: #ffffff; - mask-image: url('$(res)/img/e2e/normal.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 80%; + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 80%; + } } -} .mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { &::after { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 6040e1962f..b560209d14 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1002,8 +1002,7 @@ export default class EventTile extends React.Component { && (this.props.alwaysShowTimestamps || this.props.last || this.state.hover - || this.state.actionBarFocused) - || this.props.layout === Layout.Bubble; + || this.state.actionBarFocused); const timestamp = showTimestamp ? : null; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f0599c7e49..6253ae7d69 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -819,6 +819,7 @@ "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", + "Explore new ways switching layouts (including a new bubble layout)": "Explore new ways switching layouts (including a new bubble layout)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", @@ -1259,6 +1260,9 @@ "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Theme": "Theme", + "Message layout": "Message layout", + "Modern": "Modern", + "Message bubbles": "Message bubbles", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout", "Customise your appearance": "Customise your appearance", From 223b40c9d62963f60e5a4fc83c40055b7f411f15 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 14:23:00 +0100 Subject: [PATCH 065/148] Add dark theme support --- res/css/views/rooms/_EventBubbleTile.scss | 21 ++++++++++++------- res/themes/dark/css/_dark.scss | 6 ++++++ .../legacy-light/css/_legacy-light.scss | 6 ++++++ res/themes/light/css/_light.scss | 6 ++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 284f9bb70f..6d11992e48 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -26,8 +26,13 @@ limitations under the License. margin-right: calc(var(--gutterSize) + var(--avatarSize)); padding: 2px 0; + /* For replies */ + .mx_EventTile { + padding-top: 0; + } + &:hover { - background: rgb(242, 242, 242); + background: $eventbubble-bg-hover; } .mx_SenderProfile, @@ -37,7 +42,7 @@ limitations under the License. } .mx_SenderProfile { - padding: var(--gutterSize) var(--gutterSize) 0 var(--gutterSize); + padding: 0 var(--gutterSize); } .mx_EventTile_line { @@ -57,7 +62,7 @@ limitations under the License. top: 0; line-height: 1; img { - box-shadow: 0 0 0 2px #fff; + box-shadow: 0 0 0 2px $eventbubble-avatar-outline; border-radius: 50%; } } @@ -83,7 +88,7 @@ limitations under the License. right: calc(-1 * var(--avatarSize)); } - --backgroundColor: #F8FDFC; + --backgroundColor: $eventbubble-self-bg; } &[data-has-reply=true] { @@ -93,8 +98,8 @@ limitations under the License. .mx_ReplyThread_show { order: 99999; - background: white; - box-shadow: 0 0 0 var(--gutterSize) white; + /* background: white; + box-shadow: 0 0 0 var(--gutterSize) white; */ } .mx_ReplyThread { @@ -120,12 +125,12 @@ limitations under the License. } } - &:not([data-self=true]) { + &[data-self=false] { .mx_EventTile_avatar { left: calc(-1 * var(--avatarSize)); } - --backgroundColor: #F7F8F9; + --backgroundColor: $eventbubble-others-bg; } &.mx_EventTile_bubbleContainer, diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 8b5fde3bd1..e2ea8478d2 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -231,6 +231,12 @@ $groupFilterPanel-background-blur-amount: 30px; $composer-shadow-color: rgba(0, 0, 0, 0.28); +// Bubble tiles +$eventbubble-self-bg: rgba(141, 151, 165, 0.3); +$eventbubble-others-bg: rgba(141, 151, 165, 0.3); +$eventbubble-bg-hover: rgba(141, 151, 165, 0.1); +$eventbubble-avatar-outline: #15191E; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index a6b180bab4..6bfdad9e12 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -346,6 +346,12 @@ $appearance-tab-border-color: $input-darker-bg-color; $composer-shadow-color: tranparent; +// Bubble tiles +$eventbubble-self-bg: #F8FDFC; +$eventbubble-others-bg: #F7F8F9; +$eventbubble-bg-hover: rgb(242, 242, 242); +$eventbubble-avatar-outline: #fff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index d8dab9c9c4..4b1c56bd51 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -351,6 +351,12 @@ $groupFilterPanel-background-blur-amount: 20px; $composer-shadow-color: rgba(0, 0, 0, 0.04); +// Bubble tiles +$eventbubble-self-bg: #F8FDFC; +$eventbubble-others-bg: #F7F8F9; +$eventbubble-bg-hover: rgb(242, 242, 242); +$eventbubble-avatar-outline: #fff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { From d90d1ca8dbf5de3c81fb8b939a67490f679ed076 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 14:56:34 +0100 Subject: [PATCH 066/148] event list summary alignment in bubble layout --- res/css/views/rooms/_EventBubbleTile.scss | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 6d11992e48..0c204a19ae 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -14,11 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_EventTile[data-layout=bubble] { +.mx_EventTile[data-layout=bubble], +.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { --avatarSize: 32px; --gutterSize: 7px; --cornerRadius: 12px; --maxWidth: 70%; +} + +.mx_EventTile[data-layout=bubble] { position: relative; margin-top: var(--gutterSize); @@ -146,15 +150,22 @@ limitations under the License. .mx_EventTile_avatar { position: static; order: -1; + margin-right: 5px; } } & ~ .mx_EventListSummary { - --maxWidth: 95%; + --maxWidth: 80%; + margin-left: calc(var(--avatarSize) + var(--gutterSize)); + margin-right: calc(var(--gutterSize) + var(--avatarSize)); .mx_EventListSummary_toggle { float: none; margin: 0; order: 9; + margin-left: 5px; + } + .mx_EventListSummary_avatars { + padding-top: 0; } } From d804df84a7bffc331440ff7379f4cd865d513835 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 15:16:47 +0100 Subject: [PATCH 067/148] Allow missing sender in event --- src/components/views/rooms/EventTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index b560209d14..d2c6bf0ab9 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1182,7 +1182,7 @@ export default class EventTile extends React.Component { this.props.alwaysShowTimestamps || this.state.hover, ); - const isOwnEvent = this.props.mxEvent.sender.userId === MatrixClientPeg.get().getUserId(); + const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId(); // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( From 19bc44e3fbbc675b7cc897d4445b7b88e47ae27f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 16:17:09 +0100 Subject: [PATCH 068/148] fix branch matching for element-web --- scripts/fetchdep.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 0990af70ce..07efee69e6 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -46,12 +46,7 @@ BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then if [ -n "$GITHUB_HEAD_REF" ]; then - if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then - clone $deforg $defrepo $GITHUB_HEAD_REF - else - REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) - clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF - fi + clone $deforg $defrepo $GITHUB_HEAD_REF else clone $deforg $defrepo $BUILDKITE_BRANCH fi From 85399e8edfbbaeb5dd2ac239e5e72865099122d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 13:16:45 +0200 Subject: [PATCH 069/148] Match code style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MessagePanel.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index dcad9f8ce2..c575dd4d47 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -229,6 +229,9 @@ export default class MessagePanel extends React.Component { private readonly showTypingNotificationsWatcherRef: string; private eventNodes: Record; + // A map of + private callEventGroupers = new Map(); + constructor(props, context) { super(props, context); @@ -245,9 +248,6 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - - // A map of - this._callEventGroupers = new Map(); } componentDidMount() { @@ -576,12 +576,12 @@ export default class MessagePanel extends React.Component { mxEv.getType().indexOf("org.matrix.call.") === 0 ) { const callId = mxEv.getContent().call_id; - if (this._callEventGroupers.has(callId)) { - this._callEventGroupers.get(callId).add(mxEv); + if (this.callEventGroupers.has(callId)) { + this.callEventGroupers.get(callId).add(mxEv); } else { const callEventGrouper = new CallEventGrouper(); callEventGrouper.add(mxEv); - this._callEventGroupers.set(callId, callEventGrouper); + this.callEventGroupers.set(callId, callEventGrouper); } } @@ -698,7 +698,7 @@ export default class MessagePanel extends React.Component { // it's successful: we received it. isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); - const callEventGrouper = this._callEventGroupers.get(mxEv.getContent().call_id); + const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id); // use txnId as key if available so that we don't remount during sending ret.push( From 9383ecc46f9f5304a6602ff33aa74e1b07f0e146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 13:20:02 +0200 Subject: [PATCH 070/148] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/CallEventGrouper.ts | 16 ++++++++-------- src/components/views/messages/CallEvent.tsx | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index c71d1a032a..384f20cd4e 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -87,16 +87,16 @@ export default class CallEventGrouper extends EventEmitter { private onSilencedCallsChanged = () => { const newState = CallHandler.sharedInstance().isCallSilenced(this.callId); - this.emit(CallEventGrouperEvent.SilencedChanged, newState) - } + this.emit(CallEventGrouperEvent.SilencedChanged, newState); + }; public answerCall = () => { this.call?.answer(); - } + }; public rejectCall = () => { this.call?.reject(); - } + }; public callBack = () => { defaultDispatcher.dispatch({ @@ -104,14 +104,14 @@ export default class CallEventGrouper extends EventEmitter { type: this.isVoice ? CallType.Voice : CallType.Video, room_id: [...this.events][0]?.getRoomId(), }); - } + }; public toggleSilenced = () => { const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId); silenced ? CallHandler.sharedInstance().unSilenceCall(this.callId) : CallHandler.sharedInstance().silenceCall(this.callId); - } + }; private setCallListeners() { if (!this.call) return; @@ -128,7 +128,7 @@ export default class CallEventGrouper extends EventEmitter { else if (this.invite && this.call) this.state = CallState.Connecting; } this.emit(CallEventGrouperEvent.StateChanged, this.state); - } + }; private setCall = () => { if (this.call) return; @@ -136,7 +136,7 @@ export default class CallEventGrouper extends EventEmitter { this.call = CallHandler.sharedInstance().getCallById(this.callId); this.setCallListeners(); this.setState(); - } + }; public add(event: MatrixEvent) { this.events.add(event); diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index bb219c458d..d4781a7872 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -48,7 +48,7 @@ export default class CallEvent extends React.Component { this.state = { callState: this.props.callEventGrouper.state, silenced: false, - } + }; } componentDidMount() { @@ -66,7 +66,7 @@ export default class CallEvent extends React.Component { }; private onStateChanged = (newState: CallState) => { - this.setState({callState: newState}); + this.setState({ callState: newState }); }; private renderContent(state: CallState | CustomCallState): JSX.Element { @@ -138,7 +138,7 @@ export default class CallEvent extends React.Component { } else if (hangupReason === CallErrorCode.UserBusy) { reason = _t("The user you called is busy."); } else { - reason = _t('Unknown failure: %(reason)s)', {reason: hangupReason}); + reason = _t('Unknown failure: %(reason)s)', { reason: hangupReason }); } return ( @@ -191,7 +191,7 @@ export default class CallEvent extends React.Component { mx_CallEvent_type_icon: true, mx_CallEvent_type_icon_voice: isVoice, mx_CallEvent_type_icon_video: !isVoice, - }) + }); return (
    From 297116a3b760a4f2c1d91ce882f00b5d367aad78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 13:23:18 +0200 Subject: [PATCH 071/148] MORE DELINT! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/InfoTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index 4639e23fcb..58b17488b7 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -29,7 +29,7 @@ export enum InfoTooltipKind { interface ITooltipProps { tooltip?: React.ReactNode; - className?: string, + className?: string; tooltipClassName?: string; kind?: InfoTooltipKind; } From de875bbe1d39286c8a164520a3a1dd0c76aae52c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 5 Jul 2021 16:22:18 +0200 Subject: [PATCH 072/148] fix avatar position and outline --- res/css/views/rooms/_EventBubbleTile.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 0c204a19ae..c548bfae56 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_EventTile[data-layout=bubble], .mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { --avatarSize: 32px; - --gutterSize: 7px; + --gutterSize: 11px; --cornerRadius: 12px; --maxWidth: 70%; } @@ -55,9 +55,10 @@ limitations under the License. background: var(--backgroundColor); display: flex; gap: var(--gutterSize); + margin: 0 calc(-2 * var(--gutterSize)); > a { position: absolute; - left: -33px; + left: -50px; } } @@ -66,7 +67,7 @@ limitations under the License. top: 0; line-height: 1; img { - box-shadow: 0 0 0 2px $eventbubble-avatar-outline; + box-shadow: 0 0 0 3px $eventbubble-avatar-outline; border-radius: 50%; } } @@ -89,6 +90,7 @@ limitations under the License. } } .mx_EventTile_avatar { + top: -19px; // height of the sender block right: calc(-1 * var(--avatarSize)); } From b0a1fc7b9785814aa205047e2956ff40e393a244 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 11:23:38 +0200 Subject: [PATCH 073/148] Updated color scheme and spacing --- res/css/views/rooms/_EventBubbleTile.scss | 37 +++++++++++++++++------ res/themes/dark/css/_dark.scss | 8 ++--- res/themes/light/css/_light.scss | 4 +-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index c548bfae56..936092db7a 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -26,9 +26,12 @@ limitations under the License. position: relative; margin-top: var(--gutterSize); - margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); - padding: 2px 0; + margin-left: 50px; + margin-right: 50px; + + &.mx_EventTile_continuation { + margin-top: 2px; + } /* For replies */ .mx_EventTile { @@ -36,7 +39,23 @@ limitations under the License. } &:hover { - background: $eventbubble-bg-hover; + &::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -60px; + right: -65px; + z-index: -1; + background: $eventbubble-bg-hover; + border-radius: 4px; + } + + .mx_EventTile_avatar { + img { + box-shadow: 0 0 0 3px $eventbubble-bg-hover; + } + } } .mx_SenderProfile, @@ -55,10 +74,10 @@ limitations under the License. background: var(--backgroundColor); display: flex; gap: var(--gutterSize); - margin: 0 calc(-2 * var(--gutterSize)); + margin: 0 -12px 0 -22px; > a { position: absolute; - left: -50px; + left: -57px; } } @@ -91,7 +110,7 @@ limitations under the License. } .mx_EventTile_avatar { top: -19px; // height of the sender block - right: calc(-1 * var(--avatarSize)); + right: -45px; } --backgroundColor: $eventbubble-self-bg; @@ -104,8 +123,6 @@ limitations under the License. .mx_ReplyThread_show { order: 99999; - /* background: white; - box-shadow: 0 0 0 var(--gutterSize) white; */ } .mx_ReplyThread { @@ -190,7 +207,7 @@ limitations under the License. .mx_EventTile_readAvatars { position: absolute; - right: -45px; + right: -60px; bottom: 0; top: auto; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index e2ea8478d2..5ded90230b 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -232,10 +232,10 @@ $groupFilterPanel-background-blur-amount: 30px; $composer-shadow-color: rgba(0, 0, 0, 0.28); // Bubble tiles -$eventbubble-self-bg: rgba(141, 151, 165, 0.3); -$eventbubble-others-bg: rgba(141, 151, 165, 0.3); -$eventbubble-bg-hover: rgba(141, 151, 165, 0.1); -$eventbubble-avatar-outline: #15191E; +$eventbubble-self-bg: #143A34; +$eventbubble-others-bg: #394049; +$eventbubble-bg-hover: #433C23; +$eventbubble-avatar-outline: $bg-color; // ***** Mixins! ***** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 4b1c56bd51..c84126909e 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -354,8 +354,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04); // Bubble tiles $eventbubble-self-bg: #F8FDFC; $eventbubble-others-bg: #F7F8F9; -$eventbubble-bg-hover: rgb(242, 242, 242); -$eventbubble-avatar-outline: #fff; +$eventbubble-bg-hover: #FEFCF5; +$eventbubble-avatar-outline: $primary-bg-color; // ***** Mixins! ***** From 7d946ee0db5f7f1579df3955ce70403bfede0388 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 12:04:28 +0200 Subject: [PATCH 074/148] Restore action bar --- res/css/views/rooms/_EventBubbleTile.scss | 27 ++++++++++++++++++++-- res/css/views/rooms/_EventTile.scss | 14 +++++------ src/components/structures/MessagePanel.tsx | 4 +++- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 936092db7a..aa59f53b72 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -69,18 +69,32 @@ limitations under the License. } .mx_EventTile_line { + position: relative; padding: var(--gutterSize); - border-radius: var(--cornerRadius); + border-top-left-radius: var(--cornerRadius); + border-top-right-radius: var(--cornerRadius); + border-bottom-right-radius: var(--cornerRadius); background: var(--backgroundColor); display: flex; gap: var(--gutterSize); margin: 0 -12px 0 -22px; > a { position: absolute; - left: -57px; + left: -35px; } } + &.mx_EventTile_continuation .mx_EventTile_line { + border-top-left-radius: 0; + } + + &.mx_EventTile_lastInSection .mx_EventTile_line { + border-bottom-left-radius: var(--cornerRadius); + } + + + + .mx_EventTile_avatar { position: absolute; top: 0; @@ -94,6 +108,10 @@ limitations under the License. &[data-self=true] { .mx_EventTile_line { float: right; + > a { + left: auto; + right: -35px; + } } .mx_SenderProfile { display: none; @@ -153,6 +171,11 @@ limitations under the License. left: calc(-1 * var(--avatarSize)); } + .mx_MessageActionBar { + right: 0; + transform: translate3d(50%, 50%, 0); + } + --backgroundColor: $eventbubble-others-bg; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 446c524e81..548a852190 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -123,13 +123,6 @@ $hover-select-border: 4px; left: calc(-$hover-select-border); } - .mx_EventTile:hover .mx_MessageActionBar, - .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, - [data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, - .mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { - visibility: visible; - } - /* this is used for the tile for the event which is selected via the URL. * TODO: ultimately we probably want some transition on here. */ @@ -626,6 +619,13 @@ $hover-select-border: 4px; } } +.mx_EventTile:hover .mx_MessageActionBar, +.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, +[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, +.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { + visibility: visible; +} + @media only screen and (max-width: 480px) { .mx_EventTile_line, .mx_EventTile_reply { padding-left: 0; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index a0a1ac9b10..e811a8c1ce 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -644,8 +644,10 @@ export default class MessagePanel extends React.Component { } let willWantDateSeparator = false; + let lastInSection = true; if (nextEvent) { willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date()); + lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender(); } // is this a continuation of the previous message? @@ -702,7 +704,7 @@ export default class MessagePanel extends React.Component { isTwelveHour={this.props.isTwelveHour} permalinkCreator={this.props.permalinkCreator} last={last} - lastInSection={willWantDateSeparator} + lastInSection={lastInSection} lastSuccessful={isLastSuccessful} isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} From 870857f3213332c82d257f61e286a5ec52eac502 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 13:00:31 +0200 Subject: [PATCH 075/148] Right hand side border radius --- res/css/views/rooms/_EventBubbleTile.scss | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index aa59f53b72..4d189f78a3 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -68,12 +68,22 @@ limitations under the License. padding: 0 var(--gutterSize); } + &[data-self=false] { + .mx_EventTile_line { + border-bottom-right-radius: var(--cornerRadius); + } + } + &[data-self=true] { + .mx_EventTile_line { + border-bottom-left-radius: var(--cornerRadius); + } + } + .mx_EventTile_line { position: relative; padding: var(--gutterSize); border-top-left-radius: var(--cornerRadius); border-top-right-radius: var(--cornerRadius); - border-bottom-right-radius: var(--cornerRadius); background: var(--backgroundColor); display: flex; gap: var(--gutterSize); @@ -84,16 +94,19 @@ limitations under the License. } } - &.mx_EventTile_continuation .mx_EventTile_line { + &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line { border-top-left-radius: 0; } - - &.mx_EventTile_lastInSection .mx_EventTile_line { + &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line { border-bottom-left-radius: var(--cornerRadius); } - - + &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line { + border-top-right-radius: 0; + } + &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line { + border-bottom-right-radius: var(--cornerRadius); + } .mx_EventTile_avatar { position: absolute; From 6a03ab825f2478595930dd5d3d73a9b13b4dbb89 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 13:15:25 +0200 Subject: [PATCH 076/148] Fix style linting --- res/css/views/rooms/_EventBubbleTile.scss | 76 ++++++++++------------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 4d189f78a3..d78210a154 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -72,11 +72,45 @@ limitations under the License. .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); } + .mx_EventTile_avatar { + left: calc(-1 * var(--avatarSize)); + } + + .mx_MessageActionBar { + right: 0; + transform: translate3d(50%, 50%, 0); + } + + --backgroundColor: $eventbubble-others-bg; } &[data-self=true] { .mx_EventTile_line { border-bottom-left-radius: var(--cornerRadius); + float: right; + > a { + left: auto; + right: -35px; + } } + .mx_SenderProfile { + display: none; + } + .mx_ReactionsRow { + float: right; + clear: right; + display: flex; + + /* Moving the "add reaction button" before the reactions */ + > :last-child { + order: -1; + } + } + .mx_EventTile_avatar { + top: -19px; // height of the sender block + right: -45px; + } + + --backgroundColor: $eventbubble-self-bg; } .mx_EventTile_line { @@ -118,35 +152,6 @@ limitations under the License. } } - &[data-self=true] { - .mx_EventTile_line { - float: right; - > a { - left: auto; - right: -35px; - } - } - .mx_SenderProfile { - display: none; - } - .mx_ReactionsRow { - float: right; - clear: right; - display: flex; - - /* Moving the "add reaction button" before the reactions */ - > :last-child { - order: -1; - } - } - .mx_EventTile_avatar { - top: -19px; // height of the sender block - right: -45px; - } - - --backgroundColor: $eventbubble-self-bg; - } - &[data-has-reply=true] { > .mx_EventTile_line { flex-direction: column; @@ -179,19 +184,6 @@ limitations under the License. } } - &[data-self=false] { - .mx_EventTile_avatar { - left: calc(-1 * var(--avatarSize)); - } - - .mx_MessageActionBar { - right: 0; - transform: translate3d(50%, 50%, 0); - } - - --backgroundColor: $eventbubble-others-bg; - } - &.mx_EventTile_bubbleContainer, &.mx_EventTile_info, & ~ .mx_EventListSummary[data-expanded=false] { From 55896223aa23b48c18472880ea338b8b7c8ea7ef Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 15:13:58 +0200 Subject: [PATCH 077/148] unbubble some type of events --- res/css/views/rooms/_EventBubbleTile.scss | 58 +++++++-- res/css/views/rooms/_EventTile.scss | 139 +++++++++++---------- src/components/structures/MessagePanel.tsx | 1 + src/components/views/rooms/EventTile.tsx | 5 +- 4 files changed, 127 insertions(+), 76 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index d78210a154..313027bde6 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -27,7 +27,7 @@ limitations under the License. position: relative; margin-top: var(--gutterSize); margin-left: 50px; - margin-right: 50px; + margin-right: 100px; &.mx_EventTile_continuation { margin-top: 2px; @@ -45,7 +45,7 @@ limitations under the License. top: -1px; bottom: -1px; left: -60px; - right: -65px; + right: -60px; z-index: -1; background: $eventbubble-bg-hover; border-radius: 4px; @@ -65,7 +65,9 @@ limitations under the License. } .mx_SenderProfile { - padding: 0 var(--gutterSize); + position: relative; + top: -2px; + left: calc(-1 * var(--gutterSize)); } &[data-self=false] { @@ -73,7 +75,7 @@ limitations under the License. border-bottom-right-radius: var(--cornerRadius); } .mx_EventTile_avatar { - left: calc(-1 * var(--avatarSize)); + left: -48px; } .mx_MessageActionBar { @@ -107,7 +109,7 @@ limitations under the License. } .mx_EventTile_avatar { top: -19px; // height of the sender block - right: -45px; + right: -35px; } --backgroundColor: $eventbubble-self-bg; @@ -120,7 +122,7 @@ limitations under the License. border-top-right-radius: var(--cornerRadius); background: var(--backgroundColor); display: flex; - gap: var(--gutterSize); + gap: 5px; margin: 0 -12px 0 -22px; > a { position: absolute; @@ -214,6 +216,29 @@ limitations under the License. .mx_EventListSummary_avatars { padding-top: 0; } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 58px; + } + } + + /* events that do not require bubble layout */ + & ~ .mx_EventListSummary, + &.mx_EventTile_bad { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } } & + .mx_EventListSummary { @@ -229,13 +254,30 @@ limitations under the License. /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { - flex-direction: column; + display: grid; + grid-template: + "reply reply" auto + "shield body" auto + "shield link" auto + / auto 1fr; + .mx_EventTile_e2eIcon { + grid-area: shield; + } + .mx_UnknownBody { + grid-area: body; + } + .mx_EventTile_keyRequestInfo { + grid-area: link; + } + .mx_ReplyThread_wrapper { + grid-area: reply; + } } .mx_EventTile_readAvatars { position: absolute; - right: -60px; + right: -110px; bottom: 0; top: auto; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 548a852190..ca94ce86c8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -254,73 +254,6 @@ $hover-select-border: 4px; filter: none; } - .mx_EventTile_e2eIcon { - position: absolute; - top: 6px; - left: 44px; - width: 14px; - height: 14px; - display: block; - bottom: 0; - right: 0; - opacity: 0.2; - background-repeat: no-repeat; - background-size: contain; - - &::before, &::after { - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - } - - &::before { - background-color: #ffffff; - mask-image: url('$(res)/img/e2e/normal.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 80%; - } - } - - .mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; - } - - .mx_EventTile_e2eIcon_unknown { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; - } - - .mx_EventTile_e2eIcon_unencrypted { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; - } - - .mx_EventTile_e2eIcon_unauthenticated { - &::after { - mask-image: url('$(res)/img/e2e/normal.svg'); - background-color: $composer-e2e-icon-color; - } - opacity: 1; - } - .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { @@ -368,6 +301,14 @@ $hover-select-border: 4px; .mx_MImageBody { margin-right: 34px; } + + .mx_EventTile_e2eIcon { + position: absolute; + top: 6px; + left: 44px; + bottom: 0; + right: 0; + } } .mx_EventTile_bubbleContainer { @@ -431,6 +372,70 @@ $hover-select-border: 4px; cursor: pointer; } + +.mx_EventTile_e2eIcon { + position: relative; + width: 14px; + height: 14px; + display: block; + opacity: 0.2; + background-repeat: no-repeat; + background-size: contain; + + &::before, &::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 80%; + } +} + +.mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; +} + +.mx_EventTile_e2eIcon_unknown { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; +} + +.mx_EventTile_e2eIcon_unencrypted { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; +} + +.mx_EventTile_e2eIcon_unauthenticated { + &::after { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; + } + opacity: 1; +} + /* Various markdown overrides */ .mx_EventTile_body pre { diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index e811a8c1ce..cee6011e4a 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -712,6 +712,7 @@ export default class MessagePanel extends React.Component { layout={this.props.layout} enableFlair={this.props.enableFlair} showReadReceipts={this.props.showReadReceipts} + hideSender={this.props.room.getMembers().length <= 2} /> , ); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index a474686333..6db32a1ad5 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -289,6 +289,9 @@ interface IProps { // whether or not to always show timestamps alwaysShowTimestamps?: boolean; + + // whether or not to display the sender + hideSender?: boolean; } interface IState { @@ -978,7 +981,7 @@ export default class EventTile extends React.Component { ); } - if (needsSenderProfile) { + if (needsSenderProfile && this.props.hideSender !== true) { if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { sender = Date: Thu, 8 Jul 2021 13:43:59 +0200 Subject: [PATCH 078/148] Fix call button spacing issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index d83dfb39ad..c700eec42e 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -90,6 +90,7 @@ limitations under the License. .mx_CallEvent_content_button { height: 24px; padding: 0px 12px; + margin-left: 8px; } .mx_CallEvent_content_button_callBack { @@ -102,7 +103,7 @@ limitations under the License. .mx_CallEvent_iconButton { display: inline-flex; - margin-right: 16px; + margin-right: 8px; &::before { content: ''; From 9ec3d93402222a83883424ceddbaa09214c0f077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 14:19:02 +0200 Subject: [PATCH 079/148] Better handling of call types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 20 ++++++++++++-------- src/components/views/messages/CallEvent.tsx | 12 ++++++------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index c700eec42e..9c5de99aba 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -27,6 +27,18 @@ limitations under the License. box-sizing: border-box; height: 60px; + &.mx_CallEvent_voice { + .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_CallEvent_video { + .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + .mx_CallEvent_info { display: flex; flex-direction: row; @@ -68,14 +80,6 @@ limitations under the License. mask-size: contain; } } - - .mx_CallEvent_type_icon_voice::before { - mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); - } - - .mx_CallEvent_type_icon_video::before { - mask-image: url('$(res)/img/element-icons/call/video-call.svg'); - } } } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index d4781a7872..edbfdff6de 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -187,14 +187,14 @@ export default class CallEvent extends React.Component { const isVoice = this.props.callEventGrouper.isVoice; const callType = isVoice ? _t("Voice call") : _t("Video call"); const content = this.renderContent(this.state.callState); - const callTypeIconClass = classNames({ - mx_CallEvent_type_icon: true, - mx_CallEvent_type_icon_voice: isVoice, - mx_CallEvent_type_icon_video: !isVoice, + const className = classNames({ + mx_CallEvent: true, + mx_CallEvent_voice: isVoice, + mx_CallEvent_video: !isVoice, }); return ( -
    +
    { { sender }
    -
    +
    { callType }
    From 2615ea7f3f162dafa97868077d4a22217b0b773c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 14:35:06 +0200 Subject: [PATCH 080/148] Add icons to buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 30 ++++++++++++++++++--- src/components/views/messages/CallEvent.tsx | 10 +++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 9c5de99aba..fec5114a1c 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -28,13 +28,17 @@ limitations under the License. height: 60px; &.mx_CallEvent_voice { - .mx_CallEvent_type_icon::before { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } } &.mx_CallEvent_video { - .mx_CallEvent_type_icon::before { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { mask-image: url('$(res)/img/element-icons/call/video-call.svg'); } } @@ -95,10 +99,28 @@ limitations under the License. height: 24px; padding: 0px 12px; margin-left: 8px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + } + } } - .mx_CallEvent_content_button_callBack { - margin-left: 10px; // To match mx_callEvent + .mx_CallEvent_content_button_reject span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); } .mx_CallEvent_content_tooltip { diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index edbfdff6de..2d40d8cac1 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -85,18 +85,18 @@ export default class CallEvent extends React.Component { title={this.state.silenced ? _t("Sound on"): _t("Silence call")} /> - { _t("Decline") } + { _t("Decline") } - { _t("Accept") } + { _t("Accept") }
    ); @@ -168,7 +168,7 @@ export default class CallEvent extends React.Component { onClick={ this.props.callEventGrouper.callBack } kind="primary" > - { _t("Call back") } + { _t("Call back") }
    ); From 722c360de0a0cb13688a42219d1142a182ea61b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 14:42:05 +0200 Subject: [PATCH 081/148] Use the correct color for silence button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index fec5114a1c..54c7df3e0b 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -136,7 +136,7 @@ limitations under the License. height: 16px; width: 16px; - background-color: $icon-button-color; + background-color: $tertiary-fg-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; From 8f0d72335d381ad870c61ade9fcbe4edbacd272e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 17:16:02 +0200 Subject: [PATCH 082/148] Rework call silencing once again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 3125f11440..24efdd7ab5 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -165,7 +165,7 @@ export default class CallHandler extends EventEmitter { // do the async lookup when we get new information and then store these mappings here private assertedIdentityNativeUsers = new Map(); - private silencedCalls = new Map(); // callId -> silenced + private silencedCalls = new Set(); // callIds static sharedInstance() { if (!window.mxCallHandler) { @@ -228,7 +228,7 @@ export default class CallHandler extends EventEmitter { } public silenceCall(callId: string) { - this.silencedCalls.set(callId, true); + this.silencedCalls.add(callId); this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); // Don't pause audio if we have calls which are still ringing @@ -237,13 +237,13 @@ export default class CallHandler extends EventEmitter { } public unSilenceCall(callId: string) { - this.silencedCalls.set(callId, false); + this.silencedCalls.delete(callId); this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.play(AudioID.Ring); } public isCallSilenced(callId: string): boolean { - return this.silencedCalls.get(callId); + return this.silencedCalls.has(callId); } /** @@ -251,7 +251,7 @@ export default class CallHandler extends EventEmitter { * @returns {boolean} */ private areAnyCallsUnsilenced(): boolean { - return [...this.silencedCalls.values()].includes(false); + return this.calls.size > this.silencedCalls.size; } private async checkProtocols(maxTries) { @@ -478,6 +478,10 @@ export default class CallHandler extends EventEmitter { break; } + if (newState !== CallState.Ringing) { + this.silencedCalls.delete(call.callId); + } + switch (newState) { case CallState.Ringing: this.play(AudioID.Ring); @@ -646,8 +650,6 @@ export default class CallHandler extends EventEmitter { private removeCallForRoom(roomId: string) { console.log("Removing call for room ", roomId); - this.silencedCalls.delete(this.calls.get(roomId).callId); - this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.calls.delete(roomId); this.emit(CallHandlerEvent.CallsChanged, this.calls); } @@ -857,8 +859,6 @@ export default class CallHandler extends EventEmitter { console.log("Adding call for room ", mappedRoomId); this.calls.set(mappedRoomId, call); this.emit(CallHandlerEvent.CallsChanged, this.calls); - this.silencedCalls.set(call.callId, false); - this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.setCallListeners(call); // get ready to send encrypted events in the room, so if the user does answer From 718887dd272ca108b48385a65737124931d96fd4 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 10 Jul 2021 22:40:30 -0400 Subject: [PATCH 083/148] Update Emojibase and switch to IamCal (Slack-style) shortcodes for consistency with shortcodes commonly used by other platforms, as was decided in https://github.com/vector-im/element-web/issues/13857. One thing to be aware of is that the currently used version of Twemoji does not support a few of the newer emoji present in Emojibase, so these look a little out of place in the emoji picker. Optimally Twemoji would be updated at the same time, though I don't know how to do that. Signed-off-by: Robin Townsend --- package.json | 4 +- src/HtmlUtils.tsx | 18 +---- src/autocomplete/EmojiProvider.tsx | 69 ++++++++++--------- src/components/views/emojipicker/Preview.tsx | 12 ++-- .../views/emojipicker/QuickReactions.tsx | 7 +- src/emoji.ts | 25 ++++--- yarn.lock | 16 ++--- 7 files changed, 72 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index bb92ad11d8..4506579747 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,8 @@ "counterpart": "^0.18.6", "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase-data": "^5.1.1", - "emojibase-regex": "^4.1.1", + "emojibase-data": "^6.2.0", + "emojibase-regex": "^5.1.3", "escape-html": "^1.0.3", "file-saver": "^2.0.5", "filesize": "6.1.0", diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 016b557477..26aeef9dd8 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -34,7 +34,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import linkifyMatrix from './linkify-matrix'; import SettingsStore from './settings/SettingsStore'; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; -import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; +import { getEmojiFromUnicode, getShortcodes } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; @@ -78,20 +78,8 @@ function mightContainEmoji(str: string): boolean { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char: string): string { - const data = getEmojiFromUnicode(char); - return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); -} - -/** - * Returns the unicode character for an emoji shortcode - * - * @param {String} shortcode The shortcode (such as :thumbup:) - * @return {String} The emoji character; null if none exists - */ -export function shortcodeToUnicode(shortcode: string): string { - shortcode = shortcode.slice(1, shortcode.length - 1); - const data = SHORTCODE_TO_EMOJI.get(shortcode); - return data ? data.unicode : null; + const shortcodes = getShortcodes(getEmojiFromUnicode(char)); + return shortcodes.length > 0 ? `:${shortcodes[0]}:` : ''; } export function processHtmlForSending(html: string): string { diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 2fc77e9a17..edf691e151 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -25,8 +25,7 @@ import { PillCompletion } from './Components'; import { ICompletion, ISelectionRange } from './Autocompleter'; import { uniq, sortBy } from 'lodash'; import SettingsStore from "../settings/SettingsStore"; -import { shortcodeToUnicode } from '../HtmlUtils'; -import { EMOJI, IEmoji } from '../emoji'; +import { EMOJI, IEmoji, getShortcodes } from '../emoji'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; @@ -38,21 +37,26 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w] interface IEmojiShort { emoji: IEmoji; - shortname: string; + shortcode: string; + altShortcodes: string[]; _orderBy: number; } -const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => { +const EMOJI_SHORTCODES: IEmojiShort[] = EMOJI.sort((a, b) => { if (a.group === b.group) { return a.order - b.order; } return a.group - b.group; -}).map((emoji, index) => ({ - emoji, - shortname: `:${emoji.shortcodes[0]}:`, - // Include the index so that we can preserve the original order - _orderBy: index, -})); +}).map((emoji, index) => { + const [shortcode, ...altShortcodes] = getShortcodes(emoji); + return { + emoji, + shortcode: shortcode ? `:${shortcode}:` : undefined, + altShortcodes: altShortcodes.map(s => `:${s}:`), + // Include the index so that we can preserve the original order + _orderBy: index, + }; +}).filter(emoji => emoji.shortcode); function score(query, space) { const index = space.indexOf(query); @@ -69,15 +73,15 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { - keys: ['emoji.emoticon', 'shortname'], + this.matcher = new QueryMatcher(EMOJI_SHORTCODES, { + keys: ['emoji.emoticon', 'shortcode'], funcs: [ - (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases + o => o.altShortcodes.join(" "), // aliases ], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); - this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, { + this.nameMatcher = new QueryMatcher(EMOJI_SHORTCODES, { keys: ['emoji.annotation'], // For removing punctuation shouldMatchWordsOnly: true, @@ -105,34 +109,33 @@ export default class EmojiProvider extends AutocompleteProvider { const sorters = []; // make sure that emoticons come first - sorters.push((c) => score(matchedString, c.emoji.emoticon || "")); + sorters.push(c => score(matchedString, c.emoji.emoticon || "")); - // then sort by score (Infinity if matchedString not in shortname) - sorters.push((c) => score(matchedString, c.shortname)); + // then sort by score (Infinity if matchedString not in shortcode) + sorters.push(c => score(matchedString, c.shortcode)); // then sort by max score of all shortcodes, trim off the `:` - sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)))); - // If the matchedString is not empty, sort by length of shortname. Example: + sorters.push(c => Math.min( + ...[c.shortcode, ...c.altShortcodes].map(s => score(matchedString.substring(1), s)), + )); + // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" // completions = [":bookmark:", ":bookmark_tabs:", ...] if (matchedString.length > 1) { - sorters.push((c) => c.shortname.length); + sorters.push(c => c.shortcode.length); } // Finally, sort by original ordering - sorters.push((c) => c._orderBy); + sorters.push(c => c._orderBy); completions = sortBy(uniq(completions), sorters); - completions = completions.map(({ shortname }) => { - const unicode = shortcodeToUnicode(shortname); - return { - completion: unicode, - component: ( - - { unicode } - - ), - range, - }; - }).slice(0, LIMIT); + completions = completions.map(c => ({ + completion: c.emoji.unicode, + component: ( + + { c.emoji.unicode } + + ), + range, + })).slice(0, LIMIT); } return completions; } diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index 9c2dbb9cbd..bd9982e50f 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; -import { IEmoji } from "../../../emoji"; +import { IEmoji, getShortcodes } from "../../../emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { @@ -30,8 +30,8 @@ class Preview extends React.PureComponent { const { unicode = "", annotation = "", - shortcodes: [shortcode = ""], - } = this.props.emoji || {}; + } = this.props.emoji; + const shortcode = getShortcodes(this.props.emoji)[0]; return (
    @@ -42,9 +42,9 @@ class Preview extends React.PureComponent {
    {annotation}
    -
    - {shortcode} -
    + { shortcode ? +
    {shortcode}
    : + null }
    ); diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index ffd3ce9760..2d78e3e4cf 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; -import { getEmojiFromUnicode, IEmoji } from "../../../emoji"; +import { getEmojiFromUnicode, getShortcodes, IEmoji } from "../../../emoji"; import Emoji from "./Emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -62,6 +62,7 @@ class QuickReactions extends React.Component { }; render() { + const shortcode = this.state.hover ? getShortcodes(this.state.hover)[0] : undefined; return (

    @@ -69,7 +70,9 @@ class QuickReactions extends React.Component { ? _t("Quick Reactions") : {this.state.hover.annotation} - {this.state.hover.shortcodes[0]} + { shortcode ? + {shortcode} : + null } }

    diff --git a/src/emoji.ts b/src/emoji.ts index 7caeb06d21..ac4de654f7 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -15,14 +15,14 @@ limitations under the License. */ import EMOJIBASE from 'emojibase-data/en/compact.json'; +import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json'; export interface IEmoji { annotation: string; - group: number; + group?: number; hexcode: string; - order: number; - shortcodes: string[]; - tags: string[]; + order?: number; + tags?: string[]; unicode: string; emoticon?: string; } @@ -34,10 +34,14 @@ interface IEmojiWithFilterString extends IEmoji { // The unicode is stored without the variant selector const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode export const EMOTICON_TO_EMOJI = new Map(); -export const SHORTCODE_TO_EMOJI = new Map(); export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode)); +const toArray = (shortcodes?: string | string[]): string[] => + typeof shortcodes === "string" ? [shortcodes] : (shortcodes ?? []); +export const getShortcodes = (emoji: IEmoji): string[] => + toArray(SHORTCODES[emoji.hexcode]); + const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ "people", // smileys "people", // actually people @@ -66,12 +70,14 @@ const ZERO_WIDTH_JOINER = "\u200D"; // Store various mappings from unicode/emoticon/shortcode to the Emoji objects EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { + const shortcodes = getShortcodes(emoji); const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group]; if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { DATA_BY_CATEGORY[categoryId].push(emoji); } + // This is used as the string to match the query against when filtering emojis - emoji.filterString = (`${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + + emoji.filterString = (`${emoji.annotation}\n${shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(); // Add mapping from unicode to Emoji object @@ -87,13 +93,6 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { // Add mapping from emoticon to Emoji object EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji); } - - if (emoji.shortcodes) { - // Add mapping from each shortcode to Emoji object - emoji.shortcodes.forEach(shortcode => { - SHORTCODE_TO_EMOJI.set(shortcode, emoji); - }); - } }); /** diff --git a/yarn.lock b/yarn.lock index 90f415673d..21dc0f8fd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3022,15 +3022,15 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emojibase-data@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-5.1.1.tgz#0a0d63dd07ce1376b3d27642f28cafa46f651de6" - integrity sha512-za/ma5SfogHjwUmGFnDbTvSfm8GGFvFaPS27GPti16YZSp5EPgz+UDsZCATXvJGit+oRNBbG/FtybXHKi2UQgQ== +emojibase-data@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-6.2.0.tgz#db6c75c36905284fa623f4aa5f468d2be6ed364a" + integrity sha512-SWKaXD2QeQs06IE7qfJftsI5924Dqzp+V9xaa5RzZIEWhmlrG6Jt2iKwfgOPHu+5S8MEtOI7GdpKsXj46chXOw== -emojibase-regex@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-4.1.1.tgz#6e781aca520281600fe7a177f1582c33cf1fc545" - integrity sha512-KSigB1zQkNKFygLZ5bAfHs87LJa1ni8QTQtq8lc53Y74NF3Dk2r7kfa8MpooTO8JBb5Xz660X4tSjDB+I+7elA== +emojibase-regex@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-5.1.3.tgz#f0ef621ed6ec624becd2326f999fd4ea01b94554" + integrity sha512-gT8T9LxLA8VJdI+8KQtyykB9qKzd7WuUL3M2yw6y9tplFeufOUANg3UKVaKUvkMcRNvZsSElWhxcJrx8WPE12g== encoding@^0.1.11: version "0.1.13" From 1061cb0ffb2a2122411f4b075848edd442356407 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 13 Jul 2021 10:15:12 +0200 Subject: [PATCH 084/148] Fix layout regressions in message bubbles --- res/css/views/rooms/_EventBubbleTile.scss | 44 ++++++++++++++++--- res/themes/dark/css/_dark.scss | 1 + .../legacy-light/css/_legacy-light.scss | 1 + res/themes/light/css/_light.scss | 1 + 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 313027bde6..48011951cc 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -67,7 +67,7 @@ limitations under the License. .mx_SenderProfile { position: relative; top: -2px; - left: calc(-1 * var(--gutterSize)); + left: 2px; } &[data-self=false] { @@ -75,7 +75,7 @@ limitations under the License. border-bottom-right-radius: var(--cornerRadius); } .mx_EventTile_avatar { - left: -48px; + left: -34px; } .mx_MessageActionBar { @@ -91,7 +91,7 @@ limitations under the License. float: right; > a { left: auto; - right: -35px; + right: -48px; } } .mx_SenderProfile { @@ -123,10 +123,10 @@ limitations under the License. background: var(--backgroundColor); display: flex; gap: 5px; - margin: 0 -12px 0 -22px; + margin: 0 -12px 0 -9px; > a { position: absolute; - left: -35px; + left: -48px; } } @@ -167,6 +167,7 @@ limitations under the License. margin: 0 calc(-1 * var(--gutterSize)); .mx_EventTile_reply { + max-width: 90%; padding: 0; > a { display: none !important; @@ -186,6 +187,23 @@ limitations under the License. } } + .mx_EditMessageComposer_buttons { + position: static; + padding: 0; + margin: 0; + background: transparent; + } + + .mx_ReactionsRow { + margin-right: -18px; + margin-left: -9px; + } + + .mx_ReplyThread { + border-left-width: 2px; + border-left-color: $eventbubble-reply-color; + } + &.mx_EventTile_bubbleContainer, &.mx_EventTile_info, & ~ .mx_EventListSummary[data-expanded=false] { @@ -225,6 +243,19 @@ limitations under the License. .mx_EventTile { margin: 0 58px; } + + .mx_EventTile_line { + margin: 0 5px; + > a { + left: auto; + right: 0; + transform: translateX(calc(100% + 5px)); + } + } + + .mx_MessageActionBar { + transform: translate3d(50%, 0, 0); + } } /* events that do not require bubble layout */ @@ -283,7 +314,6 @@ limitations under the License. } .mx_MTextBody { - /* 30px equates to the width of the timestamp */ - max-width: calc(100% - 35px - var(--gutterSize)); + max-width: 100%; } } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 0b3444c95b..a43936c46e 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -234,6 +234,7 @@ $eventbubble-self-bg: #143A34; $eventbubble-others-bg: #394049; $eventbubble-bg-hover: #433C23; $eventbubble-avatar-outline: $bg-color; +$eventbubble-reply-color: #C1C6CD; // ***** Mixins! ***** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index e485028774..f349a804a8 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -352,6 +352,7 @@ $eventbubble-self-bg: #F8FDFC; $eventbubble-others-bg: #F7F8F9; $eventbubble-bg-hover: rgb(242, 242, 242); $eventbubble-avatar-outline: #fff; +$eventbubble-reply-color: #C1C6CD; // ***** Mixins! ***** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 6f0bcadaf7..ef5f4d8c86 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -354,6 +354,7 @@ $eventbubble-self-bg: #F8FDFC; $eventbubble-others-bg: #F7F8F9; $eventbubble-bg-hover: #FEFCF5; $eventbubble-avatar-outline: $primary-bg-color; +$eventbubble-reply-color: #C1C6CD; // ***** Mixins! ***** From 290174b0313cf42391525d6b2798fa4ac1a287c7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 13 Jul 2021 10:36:35 +0200 Subject: [PATCH 085/148] fix group layout and IRC layout regressions --- res/css/views/rooms/_EventTile.scss | 70 ++++++++++++++--------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index bd5b8113a9..e9d71d557c 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -25,7 +25,7 @@ $hover-select-border: 4px; font-size: $font-14px; position: relative; - .mx_EventTile.mx_EventTile_info { + &.mx_EventTile_info { padding-top: 1px; } @@ -36,12 +36,12 @@ $hover-select-border: 4px; user-select: none; } - .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { + &.mx_EventTile_info .mx_EventTile_avatar { top: $font-6px; left: $left-gutter; } - .mx_EventTile_continuation { + &.mx_EventTile_continuation { padding-top: 0px !important; &.mx_EventTile_isEditing { @@ -50,11 +50,11 @@ $hover-select-border: 4px; } } - .mx_EventTile_isEditing { + &.mx_EventTile_isEditing { background-color: $header-panel-bg-color; } - .mx_EventTile .mx_SenderProfile { + .mx_SenderProfile { color: $primary-fg-color; font-size: $font-14px; display: inline-block; /* anti-zalgo, with overflow hidden */ @@ -69,7 +69,7 @@ $hover-select-border: 4px; max-width: calc(100% - $left-gutter); } - .mx_EventTile .mx_SenderProfile .mx_Flair { + .mx_SenderProfile .mx_Flair { opacity: 0.7; margin-left: 5px; display: inline-block; @@ -84,11 +84,11 @@ $hover-select-border: 4px; } } - .mx_EventTile_isEditing .mx_MessageTimestamp { + &.mx_EventTile_isEditing .mx_MessageTimestamp { visibility: hidden; } - .mx_EventTile .mx_MessageTimestamp { + .mx_MessageTimestamp { display: block; white-space: nowrap; left: 0px; @@ -96,7 +96,7 @@ $hover-select-border: 4px; user-select: none; } - .mx_EventTile_continuation .mx_EventTile_line { + &.mx_EventTile_continuation .mx_EventTile_line { clear: both; } @@ -119,21 +119,21 @@ $hover-select-border: 4px; margin-right: 10px; } - .mx_EventTile_selected > div > a > .mx_MessageTimestamp { + &.mx_EventTile_selected > div > a > .mx_MessageTimestamp { left: calc(-$hover-select-border); } /* this is used for the tile for the event which is selected via the URL. * TODO: ultimately we probably want some transition on here. */ - .mx_EventTile_selected > .mx_EventTile_line { + &.mx_EventTile_selected > .mx_EventTile_line { border-left: $accent-color 4px solid; padding-left: calc($left-gutter - $hover-select-border); background-color: $event-selected-color; } - .mx_EventTile_highlight, - .mx_EventTile_highlight .markdown-body { + &.mx_EventTile_highlight, + &.mx_EventTile_highlight .markdown-body { color: $event-highlight-fg-color; .mx_EventTile_line { @@ -141,17 +141,17 @@ $hover-select-border: 4px; } } - .mx_EventTile_info .mx_EventTile_line { + &.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px); } - .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { + &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } - .mx_EventTile:hover .mx_EventTile_line, - .mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, - .mx_EventTile.focus-visible:focus-within .mx_EventTile_line { + &.mx_EventTile:hover .mx_EventTile_line, + &.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, + &.mx_EventTile.focus-visible:focus-within .mx_EventTile_line { background-color: $event-selected-color; } @@ -195,7 +195,7 @@ $hover-select-border: 4px; mask-image: url('$(res)/img/element-icons/circle-sending.svg'); } - .mx_EventTile_contextual { + &.mx_EventTile_contextual { opacity: 0.4; } @@ -254,46 +254,46 @@ $hover-select-border: 4px; filter: none; } - .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, - .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, - .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + &:hover.mx_EventTile_verified .mx_EventTile_line, + &:hover.mx_EventTile_unverified .mx_EventTile_line, + &:hover.mx_EventTile_unknown .mx_EventTile_line { padding-left: calc($left-gutter - $hover-select-border); } - .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { + &:hover.mx_EventTile_verified .mx_EventTile_line { border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; } - .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { + &:hover.mx_EventTile_unverified .mx_EventTile_line { border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; } - .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + &:hover.mx_EventTile_unknown .mx_EventTile_line { border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; } - .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, - .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, - .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { + &:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, + &:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, + &:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } /* End to end encryption stuff */ - .mx_EventTile:hover .mx_EventTile_e2eIcon { + &:hover .mx_EventTile_e2eIcon { opacity: 1; } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) - .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, - .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, - .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { + &:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, + &:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, + &:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { left: calc(-$hover-select-border); } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) - .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, - .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, - .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { + &:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, + &:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, + &:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { display: block; left: 41px; } From fc270b435cd559972cff5ece78613de2dc869433 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 15:32:35 +0200 Subject: [PATCH 086/148] fix group layout --- res/css/views/rooms/_EventBubbleTile.scss | 6 +++++- res/css/views/rooms/_EventTile.scss | 26 +++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 48011951cc..c66f635ffe 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -241,7 +241,7 @@ limitations under the License. } .mx_EventTile { - margin: 0 58px; + margin: 0 6px; } .mx_EventTile_line { @@ -258,6 +258,10 @@ limitations under the License. } } + & ~ .mx_EventListSummary[data-expanded=false] { + padding: 0 34px; + } + /* events that do not require bubble layout */ & ~ .mx_EventListSummary, &.mx_EventTile_bad { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index e9d71d557c..d6ad37f6bb 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -106,15 +106,6 @@ $hover-select-border: 4px; border-radius: 8px; } - .mx_RoomView_timeline_rr_enabled, - // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter - .mx_EventListSummary { - .mx_EventTile_line { - /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ - margin-right: 110px; - } - } - .mx_EventTile_reply { margin-right: 10px; } @@ -309,6 +300,23 @@ $hover-select-border: 4px; bottom: 0; right: 0; } + + .mx_ReactionsRow { + margin: 0; + padding: 6px 60px; + } +} + +.mx_RoomView_timeline_rr_enabled { + + .mx_EventTile:not([data-layout=bubble]) { + .mx_EventTile_line { + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + } + } + + // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } .mx_EventTile_bubbleContainer { From f4dfe9832bce35ebb15287eaba39e9c0c24d44e6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 16:20:25 +0200 Subject: [PATCH 087/148] change labs flag wording --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4c113aae18..0839c7eec4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -820,7 +820,7 @@ "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", - "Explore new ways switching layouts (including a new bubble layout)": "Explore new ways switching layouts (including a new bubble layout)", + "New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a3a184b908..c15ec684ad 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -325,7 +325,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { "feature_new_layout_switcher": { isFeature: true, supportedLevels: LEVELS_FEATURE, - displayName: _td("Explore new ways switching layouts (including a new bubble layout)"), + displayName: _td("New layout switcher (with message bubbles)"), default: false, controller: new NewLayoutSwitcherController(), }, From a6120ef3b780a586e148dd21237e30c26a57f363 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 16:32:29 +0200 Subject: [PATCH 088/148] Revert fetchdep script diff --- scripts/fetchdep.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 07efee69e6..0990af70ce 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -46,7 +46,12 @@ BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then if [ -n "$GITHUB_HEAD_REF" ]; then - clone $deforg $defrepo $GITHUB_HEAD_REF + if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then + clone $deforg $defrepo $GITHUB_HEAD_REF + else + REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) + clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF + fi else clone $deforg $defrepo $BUILDKITE_BRANCH fi From dde58d449dd22410b9d2fabf8359c2781d020b34 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 17:16:13 +0200 Subject: [PATCH 089/148] Only hide sender when in bubble mode --- src/components/structures/MessagePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index cee6011e4a..bf5a47cff3 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -712,7 +712,7 @@ export default class MessagePanel extends React.Component { layout={this.props.layout} enableFlair={this.props.enableFlair} showReadReceipts={this.props.showReadReceipts} - hideSender={this.props.room.getMembers().length <= 2} + hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble} /> , ); From d3823305ccb68e2639f6c0ee0e4e860ce42b3f5a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 14 Jul 2021 16:21:02 +0100 Subject: [PATCH 090/148] Upgrade matrix-js-sdk to 12.1.0-rc.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 27c4f39a09..d7933e4c59 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.0.1", + "matrix-js-sdk": "12.1.0-rc.1", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 96c02681fd..432e25cf34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5455,10 +5455,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.1.tgz#3a63881f743420a4d39474daa39bd0fb90930d43" - integrity sha512-HkOWv8QHojceo3kPbC+vAIFUjsRAig6MBvEY35UygS3g2dL0UcJ5Qx09/2wcXtu6dowlDnWsz2HHk62tS2cklA== +matrix-js-sdk@12.1.0-rc.1: + version "12.1.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.1.0-rc.1.tgz#4bc4e2342525c622e1a87b264e6f55560632b90c" + integrity sha512-F7d1e1Bm8zZqXkTKIyNeT4uA85u65nfrW2b8NwDMV+gtKNF0DOzUfUzOGD7CnjJKpyKNTQluUiwka+bXiGAVkw== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 70c93a6fee88325d954a78fce9ecaf5815a6eb53 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 14 Jul 2021 16:27:34 +0100 Subject: [PATCH 091/148] Prepare changelog for v3.26.0-rc.1 --- CHANGELOG.md | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b35b7c59..392968c906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,145 @@ +Changes in [3.26.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0-rc.1) (2021-07-14) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0...v3.26.0-rc.1) + + * Fix voice messages in right panels + [\#6370](https://github.com/matrix-org/matrix-react-sdk/pull/6370) + * Use TileShape enum more universally + [\#6369](https://github.com/matrix-org/matrix-react-sdk/pull/6369) + * Translations update from Weblate + [\#6373](https://github.com/matrix-org/matrix-react-sdk/pull/6373) + * Hide world readable history option in encrypted rooms + [\#5947](https://github.com/matrix-org/matrix-react-sdk/pull/5947) + * Make the Image View buttons easier to hit + [\#6372](https://github.com/matrix-org/matrix-react-sdk/pull/6372) + * Reorder buttons in the Image View + [\#6368](https://github.com/matrix-org/matrix-react-sdk/pull/6368) + * Add VS Code to gitignore + [\#6367](https://github.com/matrix-org/matrix-react-sdk/pull/6367) + * Fix inviter exploding due to member being null + [\#6362](https://github.com/matrix-org/matrix-react-sdk/pull/6362) + * Increase sample count in voice message thumbnail + [\#6359](https://github.com/matrix-org/matrix-react-sdk/pull/6359) + * Improve arraySeed utility + [\#6360](https://github.com/matrix-org/matrix-react-sdk/pull/6360) + * Convert FontManager to TS and stub it out for tests + [\#6358](https://github.com/matrix-org/matrix-react-sdk/pull/6358) + * Adjust recording waveform behaviour for voice messages + [\#6357](https://github.com/matrix-org/matrix-react-sdk/pull/6357) + * Do not honor string power levels + [\#6245](https://github.com/matrix-org/matrix-react-sdk/pull/6245) + * Add alias and directory customisation points + [\#6343](https://github.com/matrix-org/matrix-react-sdk/pull/6343) + * Fix multiinviter user already in room and clean up code + [\#6354](https://github.com/matrix-org/matrix-react-sdk/pull/6354) + * Fix right panel not closing user info when changing rooms + [\#6341](https://github.com/matrix-org/matrix-react-sdk/pull/6341) + * Quit sticker picker on m.sticker + [\#5679](https://github.com/matrix-org/matrix-react-sdk/pull/5679) + * Don't autodetect language in inline code blocks + [\#6350](https://github.com/matrix-org/matrix-react-sdk/pull/6350) + * Make ghost button background transparent + [\#6331](https://github.com/matrix-org/matrix-react-sdk/pull/6331) + * only consider valid & loaded url previews for show N more prompt + [\#6346](https://github.com/matrix-org/matrix-react-sdk/pull/6346) + * Extract MXCs from _matrix/media/r0/ URLs for inline images in messages + [\#6335](https://github.com/matrix-org/matrix-react-sdk/pull/6335) + * Fix small visual regression with the site name on url previews + [\#6342](https://github.com/matrix-org/matrix-react-sdk/pull/6342) + * Make PIP CallView draggable/movable + [\#5952](https://github.com/matrix-org/matrix-react-sdk/pull/5952) + * Convert VoiceUserSettingsTab to TS + [\#6340](https://github.com/matrix-org/matrix-react-sdk/pull/6340) + * Simplify typescript definition for Modernizr + [\#6339](https://github.com/matrix-org/matrix-react-sdk/pull/6339) + * Remember the last used server for room directory searches + [\#6322](https://github.com/matrix-org/matrix-react-sdk/pull/6322) + * Focus composer after reacting + [\#6332](https://github.com/matrix-org/matrix-react-sdk/pull/6332) + * Fix bug which prevented more than one event getting pinned + [\#6336](https://github.com/matrix-org/matrix-react-sdk/pull/6336) + * Make DeviceListener also update on megolm key in SSSS + [\#6337](https://github.com/matrix-org/matrix-react-sdk/pull/6337) + * Improve URL previews + [\#6326](https://github.com/matrix-org/matrix-react-sdk/pull/6326) + * Don't close settings dialog when opening spaces feedback prompt + [\#6334](https://github.com/matrix-org/matrix-react-sdk/pull/6334) + * Update import location for types + [\#6330](https://github.com/matrix-org/matrix-react-sdk/pull/6330) + * Improve blurhash rendering performance + [\#6329](https://github.com/matrix-org/matrix-react-sdk/pull/6329) + * Use a proper color scheme for codeblocks + [\#6320](https://github.com/matrix-org/matrix-react-sdk/pull/6320) + * Burn `sdk.getComponent()` with 🔥 + [\#6308](https://github.com/matrix-org/matrix-react-sdk/pull/6308) + * Fix instances of the Edit Message Composer's save button being wrongly + disabled + [\#6307](https://github.com/matrix-org/matrix-react-sdk/pull/6307) + * Do not generate a lockfile when running in CI + [\#6327](https://github.com/matrix-org/matrix-react-sdk/pull/6327) + * Update lockfile with correct dependencies + [\#6324](https://github.com/matrix-org/matrix-react-sdk/pull/6324) + * Clarify the keys we use when submitting rageshakes + [\#6321](https://github.com/matrix-org/matrix-react-sdk/pull/6321) + * Fix ImageView context menu + [\#6318](https://github.com/matrix-org/matrix-react-sdk/pull/6318) + * TypeScript migration + [\#6315](https://github.com/matrix-org/matrix-react-sdk/pull/6315) + * Move animation to compositor + [\#6310](https://github.com/matrix-org/matrix-react-sdk/pull/6310) + * Reorganize preferences + [\#5742](https://github.com/matrix-org/matrix-react-sdk/pull/5742) + * Fix being able to un-rotate images + [\#6313](https://github.com/matrix-org/matrix-react-sdk/pull/6313) + * Fix icon size in passphrase prompt + [\#6312](https://github.com/matrix-org/matrix-react-sdk/pull/6312) + * Use sleep & defer from js-sdk instead of duplicating it + [\#6305](https://github.com/matrix-org/matrix-react-sdk/pull/6305) + * Convert EventTimeline, EventTimelineSet and TimelineWindow to TS + [\#6295](https://github.com/matrix-org/matrix-react-sdk/pull/6295) + * Comply with new member-delimiter-style rule + [\#6306](https://github.com/matrix-org/matrix-react-sdk/pull/6306) + * Fix Test Linting + [\#6304](https://github.com/matrix-org/matrix-react-sdk/pull/6304) + * Convert Markdown to TypeScript + [\#6303](https://github.com/matrix-org/matrix-react-sdk/pull/6303) + * Convert RoomHeader to TS + [\#6302](https://github.com/matrix-org/matrix-react-sdk/pull/6302) + * Prevent RoomDirectory from exploding when filterString is wrongly nulled + [\#6296](https://github.com/matrix-org/matrix-react-sdk/pull/6296) + * Add support for blurhash (MSC2448) + [\#5099](https://github.com/matrix-org/matrix-react-sdk/pull/5099) + * Remove rateLimitedFunc + [\#6300](https://github.com/matrix-org/matrix-react-sdk/pull/6300) + * Convert some Key Verification classes to TypeScript + [\#6299](https://github.com/matrix-org/matrix-react-sdk/pull/6299) + * Typescript conversion of Composer components and more + [\#6292](https://github.com/matrix-org/matrix-react-sdk/pull/6292) + * Upgrade browserlist target versions + [\#6298](https://github.com/matrix-org/matrix-react-sdk/pull/6298) + * Fix browser crashing when searching for a malformed HTML tag + [\#6297](https://github.com/matrix-org/matrix-react-sdk/pull/6297) + * Add custom audio player + [\#6264](https://github.com/matrix-org/matrix-react-sdk/pull/6264) + * Lint MXC APIs to centralise access + [\#6293](https://github.com/matrix-org/matrix-react-sdk/pull/6293) + * Remove reminescent references to the tinter + [\#6290](https://github.com/matrix-org/matrix-react-sdk/pull/6290) + * More js-sdk type consolidation + [\#6263](https://github.com/matrix-org/matrix-react-sdk/pull/6263) + * Convert MessagePanel, TimelinePanel, ScrollPanel, and more to Typescript + [\#6243](https://github.com/matrix-org/matrix-react-sdk/pull/6243) + * Migrate to `eslint-plugin-matrix-org` + [\#6285](https://github.com/matrix-org/matrix-react-sdk/pull/6285) + * Avoid cyclic dependencies by moving watchers out of constructor + [\#6287](https://github.com/matrix-org/matrix-react-sdk/pull/6287) + * Add spacing between toast buttons with cross browser support in mind + [\#6284](https://github.com/matrix-org/matrix-react-sdk/pull/6284) + * Deprecate Tinter and TintableSVG + [\#6279](https://github.com/matrix-org/matrix-react-sdk/pull/6279) + * Migrate FilePanel to TypeScript + [\#6283](https://github.com/matrix-org/matrix-react-sdk/pull/6283) + Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0) From 0fe91c07b854adfed9d927310fd79a0a0077c056 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 14 Jul 2021 16:27:35 +0100 Subject: [PATCH 092/148] v3.26.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d7933e4c59..a47a337273 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.25.0", + "version": "3.26.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -198,5 +198,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From 4d16cfc951fb1c427e843965f06f8a9b1f9a558c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Jul 2021 16:41:01 +0100 Subject: [PATCH 093/148] Fix 'User' type import --- src/components/views/dialogs/VerificationRequestDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 4d3123c274..65b7f71dbd 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.tsx +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -21,7 +21,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import BaseDialog from "./BaseDialog"; import EncryptionPanel from "../right_panel/EncryptionPanel"; -import { User } from 'matrix-js-sdk'; +import { User } from 'matrix-js-sdk/src/models/user'; interface IProps { verificationRequest: VerificationRequest; From f88d5dd24e2ad6c761c0427aca1895597d6f8f02 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 16 Jul 2021 16:36:03 -0400 Subject: [PATCH 094/148] Zip shortcodes in with emoji objects Signed-off-by: Robin Townsend --- src/HtmlUtils.tsx | 4 +- src/autocomplete/EmojiProvider.tsx | 33 ++++++---------- src/components/views/emojipicker/Preview.tsx | 8 +--- .../views/emojipicker/QuickReactions.tsx | 4 +- src/emoji.ts | 38 +++++++++---------- 5 files changed, 37 insertions(+), 50 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 93cb498d21..cba9eb79b3 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -34,7 +34,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import linkifyMatrix from './linkify-matrix'; import SettingsStore from './settings/SettingsStore'; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; -import { getEmojiFromUnicode, getShortcodes } from "./emoji"; +import { getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; @@ -80,7 +80,7 @@ function mightContainEmoji(str: string): boolean { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char: string): string { - const shortcodes = getShortcodes(getEmojiFromUnicode(char)); + const shortcodes = getEmojiFromUnicode(char).shortcodes; return shortcodes.length > 0 ? `:${shortcodes[0]}:` : ''; } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index edf691e151..8a81acd498 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -25,7 +25,7 @@ import { PillCompletion } from './Components'; import { ICompletion, ISelectionRange } from './Autocompleter'; import { uniq, sortBy } from 'lodash'; import SettingsStore from "../settings/SettingsStore"; -import { EMOJI, IEmoji, getShortcodes } from '../emoji'; +import { EMOJI, IEmoji } from '../emoji'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; @@ -37,8 +37,6 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w] interface IEmojiShort { emoji: IEmoji; - shortcode: string; - altShortcodes: string[]; _orderBy: number; } @@ -47,16 +45,11 @@ const EMOJI_SHORTCODES: IEmojiShort[] = EMOJI.sort((a, b) => { return a.order - b.order; } return a.group - b.group; -}).map((emoji, index) => { - const [shortcode, ...altShortcodes] = getShortcodes(emoji); - return { - emoji, - shortcode: shortcode ? `:${shortcode}:` : undefined, - altShortcodes: altShortcodes.map(s => `:${s}:`), - // Include the index so that we can preserve the original order - _orderBy: index, - }; -}).filter(emoji => emoji.shortcode); +}).map((emoji, index) => ({ + emoji, + // Include the index so that we can preserve the original order + _orderBy: index, +})).filter(o => o.emoji.shortcodes[0]); function score(query, space) { const index = space.indexOf(query); @@ -74,10 +67,8 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); this.matcher = new QueryMatcher(EMOJI_SHORTCODES, { - keys: ['emoji.emoticon', 'shortcode'], - funcs: [ - o => o.altShortcodes.join(" "), // aliases - ], + keys: ['emoji.emoticon'], + funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`).join(" ")], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); @@ -112,16 +103,16 @@ export default class EmojiProvider extends AutocompleteProvider { sorters.push(c => score(matchedString, c.emoji.emoticon || "")); // then sort by score (Infinity if matchedString not in shortcode) - sorters.push(c => score(matchedString, c.shortcode)); + sorters.push(c => score(matchedString, c.emoji.shortcodes[0])); // then sort by max score of all shortcodes, trim off the `:` sorters.push(c => Math.min( - ...[c.shortcode, ...c.altShortcodes].map(s => score(matchedString.substring(1), s)), + ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)), )); // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" // completions = [":bookmark:", ":bookmark_tabs:", ...] if (matchedString.length > 1) { - sorters.push(c => c.shortcode.length); + sorters.push(c => c.emoji.shortcodes[0].length); } // Finally, sort by original ordering sorters.push(c => c._orderBy); @@ -130,7 +121,7 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.map(c => ({ completion: c.emoji.unicode, component: ( - + { c.emoji.unicode } ), diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index bd9982e50f..da2f8dcd89 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; -import { IEmoji, getShortcodes } from "../../../emoji"; +import { IEmoji } from "../../../emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { @@ -27,11 +27,7 @@ interface IProps { @replaceableComponent("views.emojipicker.Preview") class Preview extends React.PureComponent { render() { - const { - unicode = "", - annotation = "", - } = this.props.emoji; - const shortcode = getShortcodes(this.props.emoji)[0]; + const { unicode, annotation, shortcodes: [shortcode] } = this.props.emoji; return (
    diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index 2d78e3e4cf..9321450fc1 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; -import { getEmojiFromUnicode, getShortcodes, IEmoji } from "../../../emoji"; +import { getEmojiFromUnicode, IEmoji } from "../../../emoji"; import Emoji from "./Emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -62,7 +62,7 @@ class QuickReactions extends React.Component { }; render() { - const shortcode = this.state.hover ? getShortcodes(this.state.hover)[0] : undefined; + const shortcode = this.state.hover?.shortcodes?.[0]; return (

    diff --git a/src/emoji.ts b/src/emoji.ts index ac4de654f7..fe9e52d35f 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -22,26 +22,19 @@ export interface IEmoji { group?: number; hexcode: string; order?: number; + shortcodes: string[]; tags?: string[]; unicode: string; emoticon?: string; -} - -interface IEmojiWithFilterString extends IEmoji { - filterString?: string; + filterString: string; } // The unicode is stored without the variant selector -const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode -export const EMOTICON_TO_EMOJI = new Map(); +const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode +export const EMOTICON_TO_EMOJI = new Map(); export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode)); -const toArray = (shortcodes?: string | string[]): string[] => - typeof shortcodes === "string" ? [shortcodes] : (shortcodes ?? []); -export const getShortcodes = (emoji: IEmoji): string[] => - toArray(SHORTCODES[emoji.hexcode]); - const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ "people", // smileys "people", // actually people @@ -69,17 +62,24 @@ export const DATA_BY_CATEGORY = { const ZERO_WIDTH_JOINER = "\u200D"; // Store various mappings from unicode/emoticon/shortcode to the Emoji objects -EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { - const shortcodes = getShortcodes(emoji); +export const EMOJI: IEmoji[] = EMOJIBASE.map(emojiData => { + const shortcodeData = SHORTCODES[emojiData.hexcode]; + // Homogenize shortcodes by ensuring that everything is an array + const shortcodes = typeof shortcodeData === "string" ? [shortcodeData] : (shortcodeData ?? []); + + const emoji: IEmoji = { + ...emojiData, + shortcodes, + // This is used as the string to match the query against when filtering emojis + filterString: (`${emojiData.annotation}\n${shortcodes.join('\n')}}\n${emojiData.emoticon || ''}\n` + + `${emojiData.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(), + }; + const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group]; if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { DATA_BY_CATEGORY[categoryId].push(emoji); } - // This is used as the string to match the query against when filtering emojis - emoji.filterString = (`${emoji.annotation}\n${shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + - `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(); - // Add mapping from unicode to Emoji object // The 'unicode' field that we use in emojibase has either // VS15 or VS16 appended to any characters that can take @@ -93,6 +93,8 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { // Add mapping from emoticon to Emoji object EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji); } + + return emoji; }); /** @@ -106,5 +108,3 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { function stripVariation(str) { return str.replace(/[\uFE00-\uFE0F]$/, ""); } - -export const EMOJI: IEmoji[] = EMOJIBASE; From 0a99f76e7fc91be25a379a47f2217b2ae8f9fa4c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Jul 2021 20:51:20 -0600 Subject: [PATCH 095/148] Simple POC for moving download button to action bar --- src/components/views/messages/IMediaBody.ts | 32 +++++++ src/components/views/messages/MVideoBody.tsx | 50 ++++++----- .../views/messages/MessageActionBar.js | 22 +++++ src/components/views/messages/MessageEvent.js | 8 ++ src/utils/LazyValue.ts | 59 ++++++++++++ src/utils/MediaEventHelper.ts | 90 +++++++++++++++++++ 6 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 src/components/views/messages/IMediaBody.ts create mode 100644 src/utils/LazyValue.ts create mode 100644 src/utils/MediaEventHelper.ts diff --git a/src/components/views/messages/IMediaBody.ts b/src/components/views/messages/IMediaBody.ts new file mode 100644 index 0000000000..dcbdfff284 --- /dev/null +++ b/src/components/views/messages/IMediaBody.ts @@ -0,0 +1,32 @@ +/* +Copyright 2021 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 EventTile from "../rooms/EventTile"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; + +export interface IMediaBody { + getMediaHelper(): MediaEventHelper; +} + +export function canTileDownload(tile: EventTile): boolean { + if (!tile) return false; + + // Cast so we can check for IMediaBody interface safely. + // Note that we don't cast to the IMediaBody interface as that causes IDEs + // to complain about conditions always being true. + const tileAsAny = tile; + return !!tileAsAny.getMediaHelper; +} diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index d882bb1eb0..bb58a13c4d 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -26,10 +26,14 @@ import InlineSpinner from '../elements/InlineSpinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; import { BLURHASH_FIELD } from "../../../ContentMessages"; +import { IMediaBody } from "./IMediaBody"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; +import { MatrixEvent } from "matrix-js-sdk/src"; interface IProps { /* the MatrixEvent to show */ - mxEvent: any; + mxEvent: MatrixEvent; /* called when the video has loaded */ onHeightChanged: () => void; } @@ -45,11 +49,13 @@ interface IState { } @replaceableComponent("views.messages.MVideoBody") -export default class MVideoBody extends React.PureComponent { +export default class MVideoBody extends React.PureComponent implements IMediaBody { private videoRef = React.createRef(); + private mediaHelper: MediaEventHelper; constructor(props) { super(props); + this.state = { fetchingData: false, decryptedUrl: null, @@ -59,6 +65,8 @@ export default class MVideoBody extends React.PureComponent { posterLoading: false, blurhashUrl: null, }; + + this.mediaHelper = new MediaEventHelper(this.props.mxEvent); } thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) { @@ -82,6 +90,10 @@ export default class MVideoBody extends React.PureComponent { } } + public getMediaHelper(): MediaEventHelper { + return this.mediaHelper; + } + private getContentUrl(): string|null { const media = mediaFromContent(this.props.mxEvent.getContent()); if (media.isEncrypted) { @@ -97,7 +109,7 @@ export default class MVideoBody extends React.PureComponent { } private getThumbUrl(): string|null { - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); if (media.isEncrypted && this.state.decryptedThumbnailUrl) { @@ -139,7 +151,7 @@ export default class MVideoBody extends React.PureComponent { posterLoading: true, }); - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); if (media.hasThumbnail) { const image = new Image(); @@ -152,30 +164,22 @@ export default class MVideoBody extends React.PureComponent { async componentDidMount() { const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; - const content = this.props.mxEvent.getContent(); this.loadBlurhash(); - if (content.file !== undefined && this.state.decryptedUrl === null) { - let thumbnailPromise = Promise.resolve(null); - if (content?.info?.thumbnail_file) { - thumbnailPromise = decryptFile(content.info.thumbnail_file) - .then(blob => URL.createObjectURL(blob)); - } - + if (this.mediaHelper.media.isEncrypted && this.state.decryptedUrl === null) { try { - const thumbnailUrl = await thumbnailPromise; + const thumbnailUrl = await this.mediaHelper.thumbnailUrl.value; if (autoplay) { console.log("Preloading video"); - const decryptedBlob = await decryptFile(content.file); - const contentUrl = URL.createObjectURL(decryptedBlob); this.setState({ - decryptedUrl: contentUrl, + decryptedUrl: await this.mediaHelper.sourceUrl.value, decryptedThumbnailUrl: thumbnailUrl, - decryptedBlob: decryptedBlob, + decryptedBlob: await this.mediaHelper.sourceBlob.value, }); this.props.onHeightChanged(); } else { console.log("NOT preloading video"); + const content = this.props.mxEvent.getContent(); this.setState({ // For Chrome and Electron, we need to set some non-empty `src` to // enable the play button. Firefox does not seem to care either @@ -202,6 +206,7 @@ export default class MVideoBody extends React.PureComponent { if (this.state.decryptedThumbnailUrl) { URL.revokeObjectURL(this.state.decryptedThumbnailUrl); } + this.mediaHelper.destroy(); } private videoOnPlay = async () => { @@ -213,18 +218,15 @@ export default class MVideoBody extends React.PureComponent { // To stop subsequent download attempts fetchingData: true, }); - const content = this.props.mxEvent.getContent(); - if (!content.file) { + if (!this.mediaHelper.media.isEncrypted) { this.setState({ error: "No file given in content", }); return; } - const decryptedBlob = await decryptFile(content.file); - const contentUrl = URL.createObjectURL(decryptedBlob); this.setState({ - decryptedUrl: contentUrl, - decryptedBlob: decryptedBlob, + decryptedUrl: await this.mediaHelper.sourceUrl.value, + decryptedBlob: await this.mediaHelper.sourceBlob.value, fetchingData: false, }, () => { if (!this.videoRef.current) return; @@ -295,7 +297,7 @@ export default class MVideoBody extends React.PureComponent { onPlay={this.videoOnPlay} > - + {/**/} ); } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 7532554666..13854aebfc 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { canCancel } from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { canTileDownload } from "./IMediaBody"; const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -175,6 +176,16 @@ export default class MessageActionBar extends React.PureComponent { }); }; + onDownloadClick = async (ev) => { + // TODO: Maybe just call into MFileBody and render it as null + const src = this.props.getTile().getMediaHelper(); + const a = document.createElement("a"); + a.href = await src.sourceUrl.value; + a.download = "todo.png"; + a.target = "_blank"; + a.click(); + }; + /** * Runs a given fn on the set of possible events to test. The first event * that passes the checkFn will have fn executed on it. Both functions take @@ -267,6 +278,17 @@ export default class MessageActionBar extends React.PureComponent { key="react" />); } + + const tile = this.props.getTile && this.props.getTile(); + if (canTileDownload(tile)) { + toolbarOpts.splice(0, 0, ); + } } if (allowCancel) { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index cd071ebb34..49b50b610c 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -22,6 +22,7 @@ import { Mjolnir } from "../../../mjolnir/Mjolnir"; import RedactedBody from "./RedactedBody"; import UnknownBody from "./UnknownBody"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IMediaBody } from "./IMediaBody"; @replaceableComponent("views.messages.MessageEvent") export default class MessageEvent extends React.Component { @@ -69,6 +70,13 @@ export default class MessageEvent extends React.Component { this.forceUpdate(); }; + getMediaHelper() { + if (!this._body.current || !this._body.current.getMediaHelper) { + return null; + } + return this._body.current.getMediaHelper(); + } + render() { const bodyTypes = { 'm.text': sdk.getComponent('messages.TextualBody'), diff --git a/src/utils/LazyValue.ts b/src/utils/LazyValue.ts new file mode 100644 index 0000000000..9cdcda489a --- /dev/null +++ b/src/utils/LazyValue.ts @@ -0,0 +1,59 @@ +/* +Copyright 2021 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. +*/ + +/** + * Utility class for lazily getting a variable. + */ +export class LazyValue { + private val: T; + private prom: Promise; + private done = false; + + public constructor(private getFn: () => Promise) { + } + + /** + * Whether or not a cached value is present. + */ + public get present(): boolean { + // we use a tracking variable just in case the final value is falsey + return this.done; + } + + /** + * Gets the value without invoking a get. May be undefined until the + * value is fetched properly. + */ + public get cachedValue(): T { + return this.val; + } + + /** + * Gets a promise which resolves to the value, eventually. + */ + public get value(): Promise { + if (this.prom) return this.prom; + this.prom = this.getFn(); + + // Fork the promise chain to avoid accidentally making it return undefined always. + this.prom.then(v => { + this.val = v; + this.done = true; + }); + + return this.prom; + } +} diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts new file mode 100644 index 0000000000..316ee54edf --- /dev/null +++ b/src/utils/MediaEventHelper.ts @@ -0,0 +1,90 @@ +/* +Copyright 2021 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 { MatrixEvent } from "matrix-js-sdk/src"; +import { LazyValue } from "./LazyValue"; +import { Media, mediaFromContent } from "../customisations/Media"; +import { decryptFile } from "./DecryptFile"; +import { IMediaEventContent } from "../customisations/models/IMediaEventContent"; +import { IDestroyable } from "./IDestroyable"; + +// TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192 + +export class MediaEventHelper implements IDestroyable { + public readonly sourceUrl: LazyValue; + public readonly thumbnailUrl: LazyValue; + public readonly sourceBlob: LazyValue; + public readonly thumbnailBlob: LazyValue; + public readonly media: Media; + + public constructor(private event: MatrixEvent) { + this.sourceUrl = new LazyValue(this.prepareSourceUrl); + this.thumbnailUrl = new LazyValue(this.prepareThumbnailUrl); + this.sourceBlob = new LazyValue(this.fetchSource); + this.thumbnailBlob = new LazyValue(this.fetchThumbnail); + + this.media = mediaFromContent(this.event.getContent()); + } + + public destroy() { + if (this.media.isEncrypted) { + if (this.sourceUrl.present) URL.revokeObjectURL(this.sourceUrl.cachedValue); + if (this.thumbnailUrl.present) URL.revokeObjectURL(this.thumbnailUrl.cachedValue); + } + } + + private prepareSourceUrl = async () => { + if (this.media.isEncrypted) { + const blob = await this.sourceBlob.value; + return URL.createObjectURL(blob); + } else { + return this.media.srcHttp; + } + }; + + private prepareThumbnailUrl = async () => { + if (this.media.isEncrypted) { + const blob = await this.thumbnailBlob.value; + return URL.createObjectURL(blob); + } else { + return this.media.thumbnailHttp; + } + }; + + private fetchSource = () => { + if (this.media.isEncrypted) { + return decryptFile(this.event.getContent().file); + } + return this.media.downloadSource().then(r => r.blob()); + }; + + private fetchThumbnail = () => { + if (!this.media.hasThumbnail) return Promise.resolve(null); + + if (this.media.isEncrypted) { + const content = this.event.getContent(); + if (content.info?.thumbnail_file) { + return decryptFile(content.info.thumbnail_file); + } else { + // "Should never happen" + console.warn("Media claims to have thumbnail and is encrypted, but no thumbnail_file found"); + return Promise.resolve(null); + } + } + + return fetch(this.media.thumbnailHttp).then(r => r.blob()); + }; +} From 703cf7375912898597ec42a92ca85833c242e79a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 14:19:07 -0600 Subject: [PATCH 096/148] Convert MessageEvent to TS and hoist MediaEventHelper --- src/@types/common.ts | 3 +- .../context_menus/MessageContextMenu.tsx | 6 +- src/components/views/messages/MVideoBody.tsx | 37 +--- src/components/views/messages/MessageEvent.js | 146 --------------- .../views/messages/MessageEvent.tsx | 176 ++++++++++++++++++ src/utils/MediaEventHelper.ts | 21 +++ 6 files changed, 213 insertions(+), 176 deletions(-) delete mode 100644 src/components/views/messages/MessageEvent.js create mode 100644 src/components/views/messages/MessageEvent.tsx diff --git a/src/@types/common.ts b/src/@types/common.ts index 1fb9ba4303..36ef7a9ace 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { JSXElementConstructor } from "react"; +import React, { JSXElementConstructor } from "react"; // Based on https://stackoverflow.com/a/53229857/3532235 export type Without = {[P in Exclude]?: never}; @@ -22,3 +22,4 @@ export type XOR = (T | U) extends object ? (Without & U) | (Without< export type Writeable = { -readonly [P in keyof T]: T[P] }; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; +export type ReactAnyComponent = React.Component | React.ExoticComponent; diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 999e98f4ad..7092be43e9 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } -interface IEventTileOps { +export interface IEventTileOps { isWidgetHidden(): boolean; unhideWidget(): void; } +export interface IOperableEventTile { + getEventTileOps(): IEventTileOps; +} + interface IProps { /* the MatrixEvent associated with the context menu */ mxEvent: MatrixEvent; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index bb58a13c4d..2b873f6506 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -18,15 +18,12 @@ limitations under the License. import React from 'react'; import { decode } from "blurhash"; -import MFileBody from './MFileBody'; -import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from '../elements/InlineSpinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; import { BLURHASH_FIELD } from "../../../ContentMessages"; -import { IMediaBody } from "./IMediaBody"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { MatrixEvent } from "matrix-js-sdk/src"; @@ -36,6 +33,7 @@ interface IProps { mxEvent: MatrixEvent; /* called when the video has loaded */ onHeightChanged: () => void; + mediaEventHelper: MediaEventHelper; } interface IState { @@ -49,9 +47,8 @@ interface IState { } @replaceableComponent("views.messages.MVideoBody") -export default class MVideoBody extends React.PureComponent implements IMediaBody { +export default class MVideoBody extends React.PureComponent { private videoRef = React.createRef(); - private mediaHelper: MediaEventHelper; constructor(props) { super(props); @@ -65,8 +62,6 @@ export default class MVideoBody extends React.PureComponent impl posterLoading: false, blurhashUrl: null, }; - - this.mediaHelper = new MediaEventHelper(this.props.mxEvent); } thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) { @@ -90,10 +85,6 @@ export default class MVideoBody extends React.PureComponent impl } } - public getMediaHelper(): MediaEventHelper { - return this.mediaHelper; - } - private getContentUrl(): string|null { const media = mediaFromContent(this.props.mxEvent.getContent()); if (media.isEncrypted) { @@ -166,15 +157,15 @@ export default class MVideoBody extends React.PureComponent impl const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; this.loadBlurhash(); - if (this.mediaHelper.media.isEncrypted && this.state.decryptedUrl === null) { + if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { try { - const thumbnailUrl = await this.mediaHelper.thumbnailUrl.value; + const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value; if (autoplay) { console.log("Preloading video"); this.setState({ - decryptedUrl: await this.mediaHelper.sourceUrl.value, + decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, decryptedThumbnailUrl: thumbnailUrl, - decryptedBlob: await this.mediaHelper.sourceBlob.value, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, }); this.props.onHeightChanged(); } else { @@ -199,16 +190,6 @@ export default class MVideoBody extends React.PureComponent impl } } - componentWillUnmount() { - if (this.state.decryptedUrl) { - URL.revokeObjectURL(this.state.decryptedUrl); - } - if (this.state.decryptedThumbnailUrl) { - URL.revokeObjectURL(this.state.decryptedThumbnailUrl); - } - this.mediaHelper.destroy(); - } - private videoOnPlay = async () => { if (this.hasContentUrl() || this.state.fetchingData || this.state.error) { // We have the file, we are fetching the file, or there is an error. @@ -218,15 +199,15 @@ export default class MVideoBody extends React.PureComponent impl // To stop subsequent download attempts fetchingData: true, }); - if (!this.mediaHelper.media.isEncrypted) { + if (!this.props.mediaEventHelper.media.isEncrypted) { this.setState({ error: "No file given in content", }); return; } this.setState({ - decryptedUrl: await this.mediaHelper.sourceUrl.value, - decryptedBlob: await this.mediaHelper.sourceBlob.value, + decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, fetchingData: false, }, () => { if (!this.videoRef.current) return; diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js deleted file mode 100644 index 49b50b610c..0000000000 --- a/src/components/views/messages/MessageEvent.js +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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, { createRef } from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import SettingsStore from "../../../settings/SettingsStore"; -import { Mjolnir } from "../../../mjolnir/Mjolnir"; -import RedactedBody from "./RedactedBody"; -import UnknownBody from "./UnknownBody"; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { IMediaBody } from "./IMediaBody"; - -@replaceableComponent("views.messages.MessageEvent") -export default class MessageEvent extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - - /* a list of words to highlight */ - highlights: PropTypes.array, - - /* link URL for the highlights */ - highlightLink: PropTypes.string, - - /* should show URL previews for this event */ - showUrlPreview: PropTypes.bool, - - /* callback called when dynamic content in events are loaded */ - onHeightChanged: PropTypes.func, - - /* the shape of the tile, used */ - tileShape: PropTypes.string, // TODO: Use TileShape enum - - /* the maximum image height to use, if the event is an image */ - maxImageHeight: PropTypes.number, - - /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */ - overrideBodyTypes: PropTypes.object, - overrideEventTypes: PropTypes.object, - - /* the permalinkCreator */ - permalinkCreator: PropTypes.object, - }; - - constructor(props) { - super(props); - - this._body = createRef(); - } - - getEventTileOps = () => { - return this._body.current && this._body.current.getEventTileOps ? this._body.current.getEventTileOps() : null; - }; - - onTileUpdate = () => { - this.forceUpdate(); - }; - - getMediaHelper() { - if (!this._body.current || !this._body.current.getMediaHelper) { - return null; - } - return this._body.current.getMediaHelper(); - } - - render() { - const bodyTypes = { - 'm.text': sdk.getComponent('messages.TextualBody'), - 'm.notice': sdk.getComponent('messages.TextualBody'), - 'm.emote': sdk.getComponent('messages.TextualBody'), - 'm.image': sdk.getComponent('messages.MImageBody'), - 'm.file': sdk.getComponent('messages.MFileBody'), - 'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'), - 'm.video': sdk.getComponent('messages.MVideoBody'), - - ...(this.props.overrideBodyTypes || {}), - }; - const evTypes = { - 'm.sticker': sdk.getComponent('messages.MStickerBody'), - ...(this.props.overrideEventTypes || {}), - }; - - const content = this.props.mxEvent.getContent(); - const type = this.props.mxEvent.getType(); - const msgtype = content.msgtype; - let BodyType = RedactedBody; - if (!this.props.mxEvent.isRedacted()) { - // only resolve BodyType if event is not redacted - if (type && evTypes[type]) { - BodyType = evTypes[type]; - } else if (msgtype && bodyTypes[msgtype]) { - BodyType = bodyTypes[msgtype]; - } else if (content.url) { - // Fallback to MFileBody if there's a content URL - BodyType = bodyTypes['m.file']; - } else { - // Fallback to UnknownBody otherwise if not redacted - BodyType = UnknownBody; - } - } - - if (SettingsStore.getValue("feature_mjolnir")) { - const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; - const allowRender = localStorage.getItem(key) === "true"; - - if (!allowRender) { - const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); - const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender()); - const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain); - - if (userBanned || serverBanned) { - BodyType = sdk.getComponent('messages.MjolnirBody'); - } - } - } - - return BodyType ? : null; - } -} diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx new file mode 100644 index 0000000000..3c59e68c8b --- /dev/null +++ b/src/components/views/messages/MessageEvent.tsx @@ -0,0 +1,176 @@ +/* +Copyright 2015 - 2021 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, { createRef } from 'react'; +import * as sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; +import { Mjolnir } from "../../../mjolnir/Mjolnir"; +import RedactedBody from "./RedactedBody"; +import UnknownBody from "./UnknownBody"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IMediaBody } from "./IMediaBody"; +import { MatrixEvent } from "matrix-js-sdk/src"; +import { TileShape } from "../rooms/EventTile"; +import PermalinkConstructor from "../../../utils/permalinks/PermalinkConstructor"; +import { IOperableEventTile } from "../context_menus/MessageContextMenu"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { ReactAnyComponent } from "../../../@types/common"; +import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; + +interface IProps { + /* the MatrixEvent to show */ + mxEvent: MatrixEvent; + + /* a list of words to highlight */ + highlights: string[]; + + /* link URL for the highlights */ + highlightLink: string; + + /* should show URL previews for this event */ + showUrlPreview: boolean; + + /* callback called when dynamic content in events are loaded */ + onHeightChanged: () => void; + + /* the shape of the tile, used */ + tileShape: TileShape; + + /* the maximum image height to use, if the event is an image */ + maxImageHeight?: number; + + /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */ + overrideBodyTypes?: Record; + overrideEventTypes?: Record; + + /* the permalinkCreator */ + permalinkCreator: PermalinkConstructor; + + replacingEventId?: string; + editState?: unknown; +} + +@replaceableComponent("views.messages.MessageEvent") +export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { + private body: React.RefObject = createRef(); + private mediaHelper: MediaEventHelper; + + public constructor(props: IProps) { + super(props); + + if (MediaEventHelper.isEligible(this.props.mxEvent)) { + this.mediaHelper = new MediaEventHelper(this.props.mxEvent); + } + } + + public componentWillUnmount() { + this.mediaHelper?.destroy(); + } + + public componentDidUpdate(prevProps: Readonly) { + if (this.props.mxEvent !== prevProps.mxEvent && MediaEventHelper.isEligible(this.props.mxEvent)) { + this.mediaHelper?.destroy(); + this.mediaHelper = new MediaEventHelper(this.props.mxEvent); + } + } + + private get bodyTypes(): Record { + return { + [MsgType.Text]: sdk.getComponent('messages.TextualBody'), + [MsgType.Notice]: sdk.getComponent('messages.TextualBody'), + [MsgType.Emote]: sdk.getComponent('messages.TextualBody'), + [MsgType.Image]: sdk.getComponent('messages.MImageBody'), + [MsgType.File]: sdk.getComponent('messages.MFileBody'), + [MsgType.Audio]: sdk.getComponent('messages.MVoiceOrAudioBody'), + [MsgType.Video]: sdk.getComponent('messages.MVideoBody'), + + ...(this.props.overrideBodyTypes || {}), + }; + } + + private get evTypes(): Record { + return { + [EventType.Sticker]: sdk.getComponent('messages.MStickerBody'), + + ...(this.props.overrideEventTypes || {}), + }; + } + + public getEventTileOps = () => { + return (this.body.current as IOperableEventTile)?.getEventTileOps?.() || null; + }; + + public getMediaHelper() { + return this.mediaHelper; + } + + private onTileUpdate = () => { + this.forceUpdate(); + }; + + public render() { + const content = this.props.mxEvent.getContent(); + const type = this.props.mxEvent.getType(); + const msgtype = content.msgtype; + let BodyType: ReactAnyComponent = RedactedBody; + if (!this.props.mxEvent.isRedacted()) { + // only resolve BodyType if event is not redacted + if (type && this.evTypes[type]) { + BodyType = this.evTypes[type]; + } else if (msgtype && this.bodyTypes[msgtype]) { + BodyType = this.bodyTypes[msgtype]; + } else if (content.url) { + // Fallback to MFileBody if there's a content URL + BodyType = this.bodyTypes[MsgType.File]; + } else { + // Fallback to UnknownBody otherwise if not redacted + BodyType = UnknownBody; + } + } + + if (SettingsStore.getValue("feature_mjolnir")) { + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + const allowRender = localStorage.getItem(key) === "true"; + + if (!allowRender) { + const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); + const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender()); + const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain); + + if (userBanned || serverBanned) { + BodyType = sdk.getComponent('messages.MjolnirBody'); + } + } + } + + // @ts-ignore - this is a dynamic react component + return BodyType ? : null; + } +} diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts index 316ee54edf..b4deb1a8ce 100644 --- a/src/utils/MediaEventHelper.ts +++ b/src/utils/MediaEventHelper.ts @@ -20,6 +20,7 @@ import { Media, mediaFromContent } from "../customisations/Media"; import { decryptFile } from "./DecryptFile"; import { IMediaEventContent } from "../customisations/models/IMediaEventContent"; import { IDestroyable } from "./IDestroyable"; +import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; // TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192 @@ -87,4 +88,24 @@ export class MediaEventHelper implements IDestroyable { return fetch(this.media.thumbnailHttp).then(r => r.blob()); }; + + public static isEligible(event: MatrixEvent): boolean { + if (!event) return false; + if (event.isRedacted()) return false; + if (event.getType() === EventType.Sticker) return true; + if (event.getType() !== EventType.RoomMessage) return false; + + const content = event.getContent(); + const mediaMsgTypes: string[] = [ + MsgType.Video, + MsgType.Audio, + MsgType.Image, + MsgType.File, + ]; + if (mediaMsgTypes.includes(content.msgtype)) return true; + if (typeof(content.url) === 'string') return true; + + // Finally, it's probably not media + return false; + } } From 584ffbd32777199263ad67c6b5331edc1a03b6a2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 14:25:43 -0600 Subject: [PATCH 097/148] Fix refreshing the page not showing a download --- src/components/views/messages/MessageActionBar.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 13854aebfc..730a929ddd 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -33,6 +33,7 @@ import { canCancel } from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { canTileDownload } from "./IMediaBody"; +import {MediaEventHelper} from "../../../utils/MediaEventHelper"; const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -177,6 +178,11 @@ export default class MessageActionBar extends React.PureComponent { }; onDownloadClick = async (ev) => { + if (!this.props.getTile || !this.props.getTile().getMediaHelper) { + console.warn("Action bar triggered a download but the event tile is missing a media helper"); + return; + } + // TODO: Maybe just call into MFileBody and render it as null const src = this.props.getTile().getMediaHelper(); const a = document.createElement("a"); @@ -279,8 +285,8 @@ export default class MessageActionBar extends React.PureComponent { />); } - const tile = this.props.getTile && this.props.getTile(); - if (canTileDownload(tile)) { + // XXX: Assuming that the underlying tile will be a media event if it is eligible media. + if (MediaEventHelper.isEligible(this.props.mxEvent)) { toolbarOpts.splice(0, 0, Date: Thu, 15 Jul 2021 14:34:40 -0600 Subject: [PATCH 098/148] Clean up after POC --- src/components/views/messages/IMediaBody.ts | 11 ----------- src/components/views/messages/MessageActionBar.js | 3 +-- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/views/messages/IMediaBody.ts b/src/components/views/messages/IMediaBody.ts index dcbdfff284..27b5f24275 100644 --- a/src/components/views/messages/IMediaBody.ts +++ b/src/components/views/messages/IMediaBody.ts @@ -14,19 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import EventTile from "../rooms/EventTile"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; export interface IMediaBody { getMediaHelper(): MediaEventHelper; } - -export function canTileDownload(tile: EventTile): boolean { - if (!tile) return false; - - // Cast so we can check for IMediaBody interface safely. - // Note that we don't cast to the IMediaBody interface as that causes IDEs - // to complain about conditions always being true. - const tileAsAny = tile; - return !!tileAsAny.getMediaHelper; -} diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 730a929ddd..1cb86f168d 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -32,8 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { canCancel } from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { canTileDownload } from "./IMediaBody"; -import {MediaEventHelper} from "../../../utils/MediaEventHelper"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); From 5fce0ccd9d23e5dd8c5114b4cf2a50f506f608a5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 16:37:48 -0600 Subject: [PATCH 099/148] Convert images, audio, and voice messages over to the new helper --- src/components/views/messages/MAudioBody.tsx | 43 +++++++------ src/components/views/messages/MImageBody.tsx | 51 +++++---------- .../views/messages/MVoiceMessageBody.tsx | 64 ++----------------- 3 files changed, 41 insertions(+), 117 deletions(-) diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index bc7216f42c..4f688fd136 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -18,22 +18,22 @@ import React from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { Playback } from "../../../voice/Playback"; -import MFileBody from "./MFileBody"; import InlineSpinner from '../elements/InlineSpinner'; import { _t } from "../../../languageHandler"; -import { mediaFromContent } from "../../../customisations/Media"; -import { decryptFile } from "../../../utils/DecryptFile"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import AudioPlayer from "../audio_messages/AudioPlayer"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; +import { TileShape } from "../rooms/EventTile"; interface IProps { mxEvent: MatrixEvent; + tileShape?: TileShape; + mediaEventHelper: MediaEventHelper; } interface IState { error?: Error; playback?: Playback; - decryptedBlob?: Blob; } @replaceableComponent("views.messages.MAudioBody") @@ -46,33 +46,34 @@ export default class MAudioBody extends React.PureComponent { public async componentDidMount() { let buffer: ArrayBuffer; - const content: IMediaEventContent = this.props.mxEvent.getContent(); - const media = mediaFromContent(content); - if (media.isEncrypted) { + + try { try { - const blob = await decryptFile(content.file); + const blob = await this.props.mediaEventHelper.sourceBlob.value; buffer = await blob.arrayBuffer(); - this.setState({ decryptedBlob: blob }); } catch (e) { this.setState({ error: e }); console.warn("Unable to decrypt audio message", e); return; // stop processing the audio file } - } else { - try { - buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); - } catch (e) { - this.setState({ error: e }); - console.warn("Unable to download audio message", e); - return; // stop processing the audio file - } + } catch (e) { + this.setState({ error: e }); + console.warn("Unable to decrypt/download audio message", e); + return; // stop processing the audio file } // We should have a buffer to work with now: let's set it up - const playback = new Playback(buffer); + + // Note: we don't actually need a waveform to render an audio event, but voice messages do. + const content = this.props.mxEvent.getContent(); + const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); + + // We should have a buffer to work with now: let's set it up + const playback = new Playback(buffer, waveform); playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent); this.setState({ playback }); - // Note: the RecordingPlayback component will handle preparing the Playback class for us. + + // Note: the components later on will handle preparing the Playback class for us. } public componentWillUnmount() { @@ -103,7 +104,7 @@ export default class MAudioBody extends React.PureComponent { return ( - + {/**/} ); } diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 96c8652aee..9325c39982 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Copyright 2018, 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +20,6 @@ import { Blurhash } from "react-blurhash"; import MFileBody from './MFileBody'; import Modal from '../../../Modal'; -import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -34,6 +32,7 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent'; import ImageView from '../elements/ImageView'; import { SyncState } from 'matrix-js-sdk/src/sync.api'; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; export interface IProps { /* the MatrixEvent to show */ @@ -46,6 +45,7 @@ export interface IProps { /* the permalinkCreator */ permalinkCreator?: RoomPermalinkCreator; + mediaEventHelper: MediaEventHelper; } interface IState { @@ -257,38 +257,24 @@ export default class MImageBody extends React.Component { } } - private downloadImage(): void { + private async downloadImage() { const content = this.props.mxEvent.getContent(); - if (content.file !== undefined && this.state.decryptedUrl === null) { - let thumbnailPromise = Promise.resolve(null); - if (content.info && content.info.thumbnail_file) { - thumbnailPromise = decryptFile( - content.info.thumbnail_file, - ).then(function(blob) { - return URL.createObjectURL(blob); + if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { + try { + const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value; + this.setState({ + decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, + decryptedThumbnailUrl: thumbnailUrl, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, }); - } - let decryptedBlob; - thumbnailPromise.then((thumbnailUrl) => { - return decryptFile(content.file).then(function(blob) { - decryptedBlob = blob; - return URL.createObjectURL(blob); - }).then((contentUrl) => { - if (this.unmounted) return; - this.setState({ - decryptedUrl: contentUrl, - decryptedThumbnailUrl: thumbnailUrl, - decryptedBlob: decryptedBlob, - }); - }); - }).catch((err) => { + } catch (err) { if (this.unmounted) return; console.warn("Unable to decrypt attachment: ", err); // Set a placeholder image when we can't decrypt the image. this.setState({ error: err, }); - }); + } } } @@ -300,10 +286,10 @@ export default class MImageBody extends React.Component { localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true"; if (showImage) { - // Don't download anything becaue we don't want to display anything. + // noinspection JSIgnoredPromiseFromCall this.downloadImage(); this.setState({ showImage: true }); - } + } // else don't download anything because we don't want to display anything. this._afterComponentDidMount(); } @@ -316,13 +302,6 @@ export default class MImageBody extends React.Component { componentWillUnmount() { this.unmounted = true; this.context.removeListener('sync', this.onClientSync); - - if (this.state.decryptedUrl) { - URL.revokeObjectURL(this.state.decryptedUrl); - } - if (this.state.decryptedThumbnailUrl) { - URL.revokeObjectURL(this.state.decryptedThumbnailUrl); - } } protected messageContent( diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index bec224dd2d..65426cdad2 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -15,72 +15,16 @@ limitations under the License. */ import React from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { Playback } from "../../../voice/Playback"; -import MFileBody from "./MFileBody"; import InlineSpinner from '../elements/InlineSpinner'; import { _t } from "../../../languageHandler"; -import { mediaFromContent } from "../../../customisations/Media"; -import { decryptFile } from "../../../utils/DecryptFile"; import RecordingPlayback from "../audio_messages/RecordingPlayback"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; -import { TileShape } from "../rooms/EventTile"; - -interface IProps { - mxEvent: MatrixEvent; - tileShape?: TileShape; -} - -interface IState { - error?: Error; - playback?: Playback; - decryptedBlob?: Blob; -} +import MAudioBody from "./MAudioBody"; @replaceableComponent("views.messages.MVoiceMessageBody") -export default class MVoiceMessageBody extends React.PureComponent { - constructor(props: IProps) { - super(props); +export default class MVoiceMessageBody extends MAudioBody { - this.state = {}; - } - - public async componentDidMount() { - let buffer: ArrayBuffer; - const content: IMediaEventContent = this.props.mxEvent.getContent(); - const media = mediaFromContent(content); - if (media.isEncrypted) { - try { - const blob = await decryptFile(content.file); - buffer = await blob.arrayBuffer(); - this.setState({ decryptedBlob: blob }); - } catch (e) { - this.setState({ error: e }); - console.warn("Unable to decrypt voice message", e); - return; // stop processing the audio file - } - } else { - try { - buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); - } catch (e) { - this.setState({ error: e }); - console.warn("Unable to download voice message", e); - return; // stop processing the audio file - } - } - - const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); - - // We should have a buffer to work with now: let's set it up - const playback = new Playback(buffer, waveform); - this.setState({ playback }); - // Note: the RecordingPlayback component will handle preparing the Playback class for us. - } - - public componentWillUnmount() { - this.state.playback?.destroy(); - } + // A voice message is an audio file but rendered in a special way. public render() { if (this.state.error) { @@ -106,7 +50,7 @@ export default class MVoiceMessageBody extends React.PureComponent - + {/**/} ); } From ea7513fc16fd902d86069d45274f02dca2d292e8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 16:48:21 -0600 Subject: [PATCH 100/148] Convert MFileBody to TS and use media helper --- .../messages/{MFileBody.js => MFileBody.tsx} | 93 ++++++++++--------- 1 file changed, 47 insertions(+), 46 deletions(-) rename src/components/views/messages/{MFileBody.js => MFileBody.tsx} (86%) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.tsx similarity index 86% rename from src/components/views/messages/MFileBody.js rename to src/components/views/messages/MFileBody.tsx index 9236c77e8d..f1f004ef21 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.tsx @@ -25,6 +25,9 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; import ErrorDialog from "../dialogs/ErrorDialog"; import { TileShape } from "../rooms/EventTile"; +import { IContent, MatrixEvent } from "matrix-js-sdk/src"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on @@ -35,6 +38,7 @@ async function cacheDownloadIcon() { } // Cache the asset immediately +// noinspection JSIgnoredPromiseFromCall cacheDownloadIcon(); // User supplied content can contain scripts, we have to be careful that @@ -98,7 +102,7 @@ function computedStyle(element) { * @param {boolean} withSize Whether to include size information. Default true. * @return {string} the human readable link text for the attachment. */ -export function presentableTextForFile(content, withSize = true) { +export function presentableTextForFile(content: IContent, withSize = true): string { let linkText = _t("Attachment"); if (content.body && content.body.length > 0) { // The content body should be the name of the file including a @@ -119,53 +123,56 @@ export function presentableTextForFile(content, withSize = true) { return linkText; } -@replaceableComponent("views.messages.MFileBody") -export default class MFileBody extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - /* already decrypted blob */ - decryptedBlob: PropTypes.object, - /* called when the download link iframe is shown */ - onHeightChanged: PropTypes.func, - /* the shape of the tile, used */ - tileShape: PropTypes.string, - /* whether or not to show the default placeholder for the file. Defaults to true. */ - showGenericPlaceholder: PropTypes.bool, - }; +interface IProps { + /* the MatrixEvent to show */ + mxEvent: MatrixEvent; + /* called when the download link iframe is shown */ + onHeightChanged: () => void; + /* the shape of the tile, used */ + tileShape: TileShape; + /* whether or not to show the default placeholder for the file. Defaults to true. */ + showGenericPlaceholder: boolean; + /* helper which contains the file access */ + mediaEventHelper: MediaEventHelper; +} +interface IState { + decryptedBlob?: Blob; +} + +@replaceableComponent("views.messages.MFileBody") +export default class MFileBody extends React.Component { static defaultProps = { showGenericPlaceholder: true, }; - constructor(props) { + private iframe: React.RefObject = createRef(); + private dummyLink: React.RefObject = createRef(); + private userDidClick = false; + + public constructor(props: IProps) { super(props); - this.state = { - decryptedBlob: (this.props.decryptedBlob ? this.props.decryptedBlob : null), - }; - - this._iframe = createRef(); - this._dummyLink = createRef(); + this.state = {}; } - _getContentUrl() { + private getContentUrl(): string { const media = mediaFromContent(this.props.mxEvent.getContent()); return media.srcHttp; } - componentDidUpdate(prevProps, prevState) { + public componentDidUpdate(prevProps, prevState) { if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) { this.props.onHeightChanged(); } } - render() { - const content = this.props.mxEvent.getContent(); + public render() { + const content = this.props.mxEvent.getContent(); const text = presentableTextForFile(content); - const isEncrypted = content.file !== undefined; + const isEncrypted = this.props.mediaEventHelper.media.isEncrypted; const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); - const contentUrl = this._getContentUrl(); + const contentUrl = this.getContentUrl(); const fileSize = content.info ? content.info.size : null; const fileType = content.info ? content.info.mimetype : "application/octet-stream"; @@ -182,29 +189,23 @@ export default class MFileBody extends React.Component { } if (isEncrypted) { - if (this.state.decryptedBlob === null) { + if (!this.state.decryptedBlob) { // Need to decrypt the attachment // Wait for the user to click on the link before downloading // and decrypting the attachment. - let decrypting = false; - const decrypt = (e) => { - if (decrypting) { - return false; - } - decrypting = true; - decryptFile(content.file).then((blob) => { + const decrypt = async () => { + try { + this.userDidClick = true; this.setState({ - decryptedBlob: blob, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, }); - }).catch((err) => { + } catch (err) { console.warn("Unable to decrypt attachment: ", err); Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, { title: _t("Error"), description: _t("Error decrypting attachment"), }); - }).finally(() => { - decrypting = false; - }); + } }; // This button should actually Download because usercontent/ will try to click itself @@ -226,7 +227,7 @@ export default class MFileBody extends React.Component { ev.target.contentWindow.postMessage({ imgSrc: downloadIconUrl, imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon. - style: computedStyle(this._dummyLink.current), + style: computedStyle(this.dummyLink.current), blob: this.state.decryptedBlob, // Set a download attribute for encrypted files so that the file // will have the correct name when the user tries to download it. @@ -234,7 +235,7 @@ export default class MFileBody extends React.Component { download: fileName, textContent: _t("Download %(text)s", { text: text }), // only auto-download if a user triggered this iframe explicitly - auto: !this.props.decryptedBlob, + auto: this.userDidClick, }, "*"); }; @@ -251,12 +252,12 @@ export default class MFileBody extends React.Component { * We'll use it to learn how the download link * would have been styled if it was rendered inline. */ } - +