From c83ad1faa78ab818548f4775149269386a2f0a06 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 07:41:38 -0400 Subject: [PATCH 01/10] Add local echo of connected devices in video rooms (#8368) --- src/components/views/rooms/RoomTile.tsx | 22 +++++--- src/components/views/voip/VideoLobby.tsx | 10 ++-- src/utils/VideoChannelUtils.ts | 38 ++++++++++---- test/components/views/rooms/RoomTile-test.tsx | 51 +++++++++++++++---- .../components/views/voip/VideoLobby-test.tsx | 45 ++++++++++++---- test/test-utils/test-utils.ts | 1 + test/test-utils/video.ts | 2 +- 7 files changed, 126 insertions(+), 43 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 530b22571aa..7b0a8e95de9 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -81,7 +81,7 @@ interface IState { messagePreview?: string; videoStatus: VideoStatus; // Active video channel members, according to room state - videoMembers: RoomMember[]; + videoMembers: Set; // Active video channel members, according to Jitsi jitsiParticipants: IJitsiParticipant[]; } @@ -124,7 +124,7 @@ export default class RoomTile extends React.PureComponent { // generatePreview() will return nothing if the user has previews disabled messagePreview: "", videoStatus, - videoMembers: getConnectedMembers(this.props.room.currentState), + videoMembers: getConnectedMembers(this.props.room, videoStatus === VideoStatus.Connected), jitsiParticipants: VideoChannelStore.instance.participants, }; this.generatePreview(); @@ -593,7 +593,9 @@ export default class RoomTile extends React.PureComponent { } private updateVideoMembers = () => { - this.setState({ videoMembers: getConnectedMembers(this.props.room.currentState) }); + this.setState(state => ({ + videoMembers: getConnectedMembers(this.props.room, state.videoStatus === VideoStatus.Connected), + })); }; private updateVideoStatus = () => { @@ -610,7 +612,10 @@ export default class RoomTile extends React.PureComponent { private onConnectVideo = (roomId: string) => { if (roomId === this.props.room?.roomId) { - this.setState({ videoStatus: VideoStatus.Connected }); + this.setState({ + videoStatus: VideoStatus.Connected, + videoMembers: getConnectedMembers(this.props.room, true), + }); VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants); } }; @@ -623,7 +628,10 @@ export default class RoomTile extends React.PureComponent { private onDisconnectVideo = (roomId: string) => { if (roomId === this.props.room?.roomId) { - this.setState({ videoStatus: VideoStatus.Disconnected }); + this.setState({ + videoStatus: VideoStatus.Disconnected, + videoMembers: getConnectedMembers(this.props.room, false), + }); VideoChannelStore.instance.off(VideoChannelEvent.Participants, this.updateJitsiParticipants); } }; @@ -668,12 +676,12 @@ export default class RoomTile extends React.PureComponent { case VideoStatus.Disconnected: videoText = _t("Video"); videoActive = false; - participantCount = this.state.videoMembers.length; + participantCount = this.state.videoMembers.size; break; case VideoStatus.Connecting: videoText = _t("Connecting..."); videoActive = true; - participantCount = this.state.videoMembers.length; + participantCount = this.state.videoMembers.size; break; case VideoStatus.Connected: videoText = _t("Connected"); diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx index 84bc470273e..f9e95089270 100644 --- a/src/components/views/voip/VideoLobby.tsx +++ b/src/components/views/voip/VideoLobby.tsx @@ -110,7 +110,7 @@ const MAX_FACES = 8; const VideoLobby: FC<{ room: Room }> = ({ room }) => { const [connecting, setConnecting] = useState(false); const me = useMemo(() => room.getMember(room.myUserId), [room]); - const connectedMembers = useConnectedMembers(room.currentState); + const connectedMembers = useConnectedMembers(room, false); const videoRef = useRef(); const devices = useAsyncMemo(async () => { @@ -172,12 +172,12 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { }; let facePile; - if (connectedMembers.length) { - const shownMembers = connectedMembers.slice(0, MAX_FACES); - const overflow = connectedMembers.length > shownMembers.length; + if (connectedMembers.size) { + const shownMembers = [...connectedMembers].slice(0, MAX_FACES); + const overflow = connectedMembers.size > shownMembers.length; facePile =
- { _t("%(count)s people connected", { count: connectedMembers.length }) } + { _t("%(count)s people connected", { count: connectedMembers.size }) }
; } diff --git a/src/utils/VideoChannelUtils.ts b/src/utils/VideoChannelUtils.ts index 11a1a9a35f2..cc3c99d980c 100644 --- a/src/utils/VideoChannelUtils.ts +++ b/src/utils/VideoChannelUtils.ts @@ -17,7 +17,8 @@ limitations under the License. import { useState } from "react"; import { throttle } from "lodash"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTypedEventEmitter } from "../hooks/useEventEmitter"; @@ -42,17 +43,32 @@ export const addVideoChannel = async (roomId: string, roomName: string) => { await WidgetUtils.addJitsiWidget(roomId, CallType.Video, "Video channel", VIDEO_CHANNEL, roomName); }; -export const getConnectedMembers = (state: RoomState): RoomMember[] => - state.getStateEvents(VIDEO_CHANNEL_MEMBER) +export const getConnectedMembers = (room: Room, connectedLocalEcho: boolean): Set => { + const members = new Set(); + + for (const e of room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER)) { + const member = room.getMember(e.getStateKey()); + let devices = e.getContent()?.devices ?? []; + + // Apply local echo for the disconnected case + if (!connectedLocalEcho && member?.userId === room.client.getUserId()) { + devices = devices.filter(d => d !== room.client.getDeviceId()); + } // Must have a device connected and still be joined to the room - .filter(e => e.getContent()?.devices?.length) - .map(e => state.getMember(e.getStateKey())) - .filter(member => member?.membership === "join"); - -export const useConnectedMembers = (state: RoomState, throttleMs = 100) => { - const [members, setMembers] = useState(getConnectedMembers(state)); - useTypedEventEmitter(state, RoomStateEvent.Update, throttle(() => { - setMembers(getConnectedMembers(state)); + if (devices.length && member?.membership === "join") members.add(member); + } + + // Apply local echo for the connected case + if (connectedLocalEcho) members.add(room.getMember(room.client.getUserId())); + return members; +}; + +export const useConnectedMembers = ( + room: Room, connectedLocalEcho: boolean, throttleMs = 100, +): Set => { + const [members, setMembers] = useState>(getConnectedMembers(room, connectedLocalEcho)); + useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => { + setMembers(getConnectedMembers(room, connectedLocalEcho)); }, throttleMs, { leading: true, trailing: true })); return members; }; diff --git a/test/components/views/rooms/RoomTile-test.tsx b/test/components/views/rooms/RoomTile-test.tsx index d209c32f0f9..d07360f6d4f 100644 --- a/test/components/views/rooms/RoomTile-test.tsx +++ b/test/components/views/rooms/RoomTile-test.tsx @@ -18,6 +18,8 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { @@ -26,6 +28,7 @@ import { mkRoom, mkVideoChannelMember, stubVideoChannelStore, + StubVideoChannelStore, } from "../../../test-utils"; import RoomTile from "../../../../src/components/views/rooms/RoomTile"; import SettingsStore from "../../../../src/settings/SettingsStore"; @@ -39,9 +42,8 @@ describe("RoomTile", () => { jest.spyOn(PlatformPeg, 'get') .mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform); - let cli; - let store; - + let cli: MatrixClient; + let store: StubVideoChannelStore; beforeEach(() => { const realGetValue = SettingsStore.getValue; SettingsStore.getValue = (name: string, roomId?: string): T => { @@ -52,7 +54,7 @@ describe("RoomTile", () => { }; stubClient(); - cli = mocked(MatrixClientPeg.get()); + cli = MatrixClientPeg.get(); store = stubVideoChannelStore(); DMRoomMap.makeShared(); }); @@ -60,8 +62,11 @@ describe("RoomTile", () => { afterEach(() => jest.clearAllMocks()); describe("video rooms", () => { - const room = mkRoom(cli, "!1:example.org"); - room.isElementVideoRoom.mockReturnValue(true); + let room: Room; + beforeEach(() => { + room = mkRoom(cli, "!1:example.org"); + mocked(room.isElementVideoRoom).mockReturnValue(true); + }); it("tracks connection state", () => { const tile = mount( @@ -97,7 +102,7 @@ describe("RoomTile", () => { mkVideoChannelMember("@chris:example.org", ["device 1"]), ])); - mocked(room.currentState).getMember.mockImplementation(userId => ({ + mocked(room).getMember.mockImplementation(userId => ({ userId, membership: userId === "@chris:example.org" ? "leave" : "join", name: userId, @@ -117,8 +122,36 @@ describe("RoomTile", () => { ); // Only Alice should display as connected - const participants = tile.find(".mx_RoomTile_videoParticipants"); - expect(participants.text()).toEqual("1"); + expect(tile.find(".mx_RoomTile_videoParticipants").text()).toEqual("1"); + }); + + it("reflects local echo in connected members", () => { + mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([ + // Make the remote echo claim that we're connected, while leaving the store disconnected + mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]), + ])); + + mocked(room).getMember.mockImplementation(userId => ({ + userId, + membership: "join", + name: userId, + rawDisplayName: userId, + roomId: "!1:example.org", + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + }) as unknown as RoomMember); + + const tile = mount( + , + ); + + // Because of our local echo, we should still appear as disconnected + expect(tile.find(".mx_RoomTile_videoParticipants").exists()).toEqual(false); }); }); }); diff --git a/test/components/views/voip/VideoLobby-test.tsx b/test/components/views/voip/VideoLobby-test.tsx index 4e7afb12c44..2d69709dc76 100644 --- a/test/components/views/voip/VideoLobby-test.tsx +++ b/test/components/views/voip/VideoLobby-test.tsx @@ -18,11 +18,14 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { stubClient, stubVideoChannelStore, + StubVideoChannelStore, mkRoom, mkVideoChannelMember, mockStateEventImplementation, @@ -33,7 +36,6 @@ import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar" import VideoLobby from "../../../../src/components/views/voip/VideoLobby"; describe("VideoLobby", () => { - stubClient(); Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: jest.fn(), @@ -42,19 +44,17 @@ describe("VideoLobby", () => { }); jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {}); - const cli = MatrixClientPeg.get(); - const room = mkRoom(cli, "!1:example.org"); - - let store; + let cli: MatrixClient; + let store: StubVideoChannelStore; + let room: Room; beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.get(); store = stubVideoChannelStore(); + room = mkRoom(cli, "!1:example.org"); mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]); }); - afterEach(() => { - jest.clearAllMocks(); - }); - describe("connected members", () => { it("hides when no one is connected", async () => { const lobby = mount(); @@ -75,7 +75,7 @@ describe("VideoLobby", () => { mkVideoChannelMember("@chris:example.org", ["device 1"]), ])); - mocked(room.currentState).getMember.mockImplementation(userId => ({ + mocked(room).getMember.mockImplementation(userId => ({ userId, membership: userId === "@chris:example.org" ? "leave" : "join", name: userId, @@ -95,6 +95,31 @@ describe("VideoLobby", () => { expect(memberText).toEqual("1 person connected"); expect(lobby.find(FacePile).find(MemberAvatar).props().member.userId).toEqual("@alice:example.org"); }); + + it("doesn't include remote echo of this device being connected", async () => { + mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([ + // Make the remote echo claim that we're connected, while leaving the store disconnected + mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]), + ])); + + mocked(room).getMember.mockImplementation(userId => ({ + userId, + membership: "join", + name: userId, + rawDisplayName: userId, + roomId: "!1:example.org", + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + }) as unknown as RoomMember); + + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + // Because of our local echo, we should still appear as disconnected + expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false); + }); }); describe("device buttons", () => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index fc85a825f31..a590474ffed 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -379,6 +379,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl getJoinRule: jest.fn().mockReturnValue("invite"), loadMembersIfNeeded: jest.fn(), client, + myUserId: client?.getUserId(), canInvite: jest.fn(), } as unknown as Room; } diff --git a/test/test-utils/video.ts b/test/test-utils/video.ts index 79c657a0c60..77fdfb8fcc0 100644 --- a/test/test-utils/video.ts +++ b/test/test-utils/video.ts @@ -21,7 +21,7 @@ import { mkEvent } from "./test-utils"; import { VIDEO_CHANNEL_MEMBER } from "../../src/utils/VideoChannelUtils"; import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../src/stores/VideoChannelStore"; -class StubVideoChannelStore extends EventEmitter { +export class StubVideoChannelStore extends EventEmitter { private _roomId: string; public get roomId(): string { return this._roomId; } private _connected: boolean; From dd880df6ae07300e77ad0fef96bb0378ac6ea09d Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 07:41:58 -0400 Subject: [PATCH 02/10] Forcefully disconnect from video rooms on logout and tab close (#8375) * Forcefully disconnect from video rooms on logout * Forcefully disconnect from video rooms on tab close --- src/components/structures/MatrixChat.tsx | 2 ++ src/stores/VideoChannelStore.ts | 41 +++++++++++++----------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 624909db31d..328af853142 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -131,6 +131,7 @@ import { IConfigOptions } from "../../IConfigOptions"; import { SnakedObject } from "../../utils/SnakedObject"; import InfoDialog from '../views/dialogs/InfoDialog'; import { leaveRoomBehaviour } from "../../utils/leave-behaviour"; +import VideoChannelStore from "../../stores/VideoChannelStore"; // legacy export export { default as Views } from "../../Views"; @@ -576,6 +577,7 @@ export default class MatrixChat extends React.PureComponent { break; case 'logout': CallHandler.instance.hangupAllCalls(); + if (VideoChannelStore.instance.connected) VideoChannelStore.instance.setDisconnected(); Lifecycle.logout(); break; case 'require_registration': diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 58e18cab985..14016800487 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -171,6 +171,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient { this.connected = true; messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + window.addEventListener("beforeunload", this.setDisconnected); this.emit(VideoChannelEvent.Connect, roomId); @@ -190,6 +191,27 @@ export default class VideoChannelStore extends AsyncStoreWithClient { } }; + public setDisconnected = async () => { + this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + window.removeEventListener("beforeunload", this.setDisconnected); + + const roomId = this.roomId; + this.activeChannel = null; + this.roomId = null; + this.connected = false; + this.participants = []; + + this.emit(VideoChannelEvent.Disconnect, roomId); + + // Tell others that we're disconnected, by removing our device from room state + await this.updateDevices(roomId, devices => { + const devicesSet = new Set(devices); + devicesSet.delete(this.matrixClient.getDeviceId()); + return Array.from(devicesSet); + }); + }; + private ack = (ev: CustomEvent) => { // Even if we don't have a reply to a given widget action, we still need // to give the widget API something to acknowledge receipt @@ -208,24 +230,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient { private onHangup = async (ev: CustomEvent) => { this.ack(ev); - - this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); - this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); - - const roomId = this.roomId; - this.activeChannel = null; - this.roomId = null; - this.connected = false; - this.participants = []; - - this.emit(VideoChannelEvent.Disconnect, roomId); - - // Tell others that we're disconnected, by removing our device from room state - await this.updateDevices(roomId, devices => { - const devicesSet = new Set(devices); - devicesSet.delete(this.matrixClient.getDeviceId()); - return Array.from(devicesSet); - }); + await this.setDisconnected(); }; private onParticipants = (ev: CustomEvent) => { From 146bcdd6a6314fef883a3569a9e95989f3857818 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 12:55:32 +0100 Subject: [PATCH 03/10] Move more stuff from BK to GHA (#8372) --- .editorconfig | 3 + .github/workflows/element-build-and-test.yaml | 126 +++++++++------ .github/workflows/end-to-end-tests.yaml | 99 ++++++------ .github/workflows/netlify.yaml | 145 +++++++++--------- .github/workflows/notify-element-web.yml | 27 ++-- .github/workflows/preview_changelog.yaml | 14 +- .github/workflows/static_analysis.yaml | 88 +++++++++++ .../{test_coverage.yml => tests.yml} | 17 +- .github/workflows/typecheck.yaml | 27 ---- 9 files changed, 336 insertions(+), 210 deletions(-) create mode 100644 .github/workflows/static_analysis.yaml rename .github/workflows/{test_coverage.yml => tests.yml} (78%) delete mode 100644 .github/workflows/typecheck.yaml diff --git a/.editorconfig b/.editorconfig index 880331a09e5..56631484cd5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,3 +21,6 @@ insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/element-build-and-test.yaml b/.github/workflows/element-build-and-test.yaml index 1633aae2609..905dbedb067 100644 --- a/.github/workflows/element-build-and-test.yaml +++ b/.github/workflows/element-build-and-test.yaml @@ -3,47 +3,87 @@ # as an artifact and run integration tests. name: Element Web - Build and Test on: - pull_request: + pull_request: { } + push: + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - build: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - steps: - - uses: actions/checkout@v2 - - name: Build - run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build - - name: Upload Artifact - uses: actions/upload-artifact@v2 - with: - name: previewbuild - path: element-web/webapp - # We'll only use this in a triggered job, then we're done with it - retention-days: 1 - cypress: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Download build - uses: actions/download-artifact@v3 - with: - name: previewbuild - path: webapp - - name: Run Cypress tests - uses: cypress-io/github-action@v2 - with: - # The built in Electron runner seems to grind to a halt trying - # to run the tests, so use chrome. - browser: chrome - start: npx serve -p 8080 webapp - - name: Upload Artifact - if: failure() - uses: actions/upload-artifact@v2 - with: - name: cypress-results - path: | - cypress/screenshots - cypress/videos - cypress/synapselogs + build: + name: "Build Element-Web" + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Fetch layered build + run: scripts/ci/layered.sh + + - name: Copy config + run: cp element.io/develop/config.json config.json + working-directory: ./element-web + + - name: Build + run: CI_PACKAGE=true yarn build + working-directory: ./element-web + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: previewbuild + path: element-web/webapp + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 + + cypress: + name: "Cypress End to End Tests" + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Download build + uses: actions/download-artifact@v3 + with: + name: previewbuild + path: webapp + + - name: Run Cypress tests + uses: cypress-io/github-action@v2 + with: + # The built in Electron runner seems to grind to a halt trying + # to run the tests, so use chrome. + browser: chrome + start: npx serve -p 8080 webapp + + - name: Upload Artifact + if: failure() + uses: actions/upload-artifact@v2 + with: + name: cypress-results + path: | + cypress/screenshots + cypress/videos + cypress/synapselogs + + app-tests: + name: Element Web Integration Tests + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Run tests + run: "./scripts/ci/app-tests.sh" diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 334af1772fd..1feaf266e36 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -1,47 +1,58 @@ name: End-to-end Tests on: - # These tests won't work for non-develop branches at the moment as they - # won't pull in the right versions of other repos, so they're only enabled - # on develop. - push: - branches: [develop] - pull_request: - branches: [develop] + # These tests won't work for non-develop branches at the moment as they + # won't pull in the right versions of other repos, so they're only enabled + # on develop. + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - end-to-end: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - container: vectorim/element-web-ci-e2etests-env:latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Prepare End-to-End tests - run: ./scripts/ci/prepare-end-to-end-tests.sh - - name: Run End-to-End tests - run: ./scripts/ci/run-end-to-end-tests.sh - - name: Archive logs - uses: actions/upload-artifact@v2 - if: ${{ always() }} - with: - path: | - test/end-to-end-tests/logs/**/* - test/end-to-end-tests/synapse/installations/consent/homeserver.log - retention-days: 14 - - name: Download previous benchmark data - uses: actions/cache@v1 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - name: Store benchmark result - uses: matrix-org/github-action-benchmark@jsperfentry-1 - with: - tool: 'jsperformanceentry' - output-file-path: test/end-to-end-tests/performance-entries.json - fail-on-alert: false - comment-on-alert: false - # Only temporary to monitor where failures occur - alert-comment-cc-users: '@gsouquet' - github-token: ${{ secrets.DEPLOY_GH_PAGES }} - auto-push: ${{ github.ref == 'refs/heads/develop' }} + end-to-end: + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + container: vectorim/element-web-ci-e2etests-env:latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh + + - name: Archive logs + uses: actions/upload-artifact@v2 + if: ${{ always() }} + with: + path: | + test/end-to-end-tests/logs/**/* + test/end-to-end-tests/synapse/installations/consent/homeserver.log + retention-days: 14 + + - name: Download previous benchmark data + uses: actions/cache@v1 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + + - name: Store benchmark result + uses: matrix-org/github-action-benchmark@jsperfentry-1 + with: + tool: 'jsperformanceentry' + output-file-path: test/end-to-end-tests/performance-entries.json + fail-on-alert: false + comment-on-alert: false + # Only temporary to monitor where failures occur + alert-comment-cc-users: '@gsouquet' + github-token: ${{ secrets.DEPLOY_GH_PAGES }} + auto-push: ${{ github.ref == 'refs/heads/develop' }} diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index ec09379b6e3..1acb7e8fd14 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -2,76 +2,79 @@ # and uploading it to netlify name: Upload Preview Build to Netlify on: - workflow_run: - workflows: ["Element Web - Build and Test"] - types: - - completed + workflow_run: + workflows: [ "Element Web - Build and Test" ] + types: + - completed jobs: - build: - runs-on: ubuntu-latest - if: > - ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} - steps: - - name: "🔍 Read PR number" - id: readctx - # we need to find the PR number that corresponds to the branch, which we do by - # searching the GH API - # The workflow_run event includes a list of pull requests, but it doesn't get populated for - # forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run - run: | - head_branch='${{github.event.workflow_run.head_repository.owner.login}}:${{github.event.workflow_run.head_branch}}' - echo "head branch: $head_branch" - pulls_uri="https://api.github.com/repos/${{ github.repository }}/pulls?head=$(jq -Rr '@uri' <<<$head_branch)" - pr_number=$(curl -s -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' "$pulls_uri" | - jq -r '.[] | .number') - echo "PR number: $pr_number" - echo "::set-output name=prnumber::$pr_number" - # There's a 'download artifact' action but it hasn't been updated for the - # workflow_run action (https://github.com/actions/download-artifact/issues/60) - # so instead we get this mess: - - name: 'Download artifact' - uses: actions/github-script@v3.1.0 - with: - script: | - var artifacts = await github.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{github.event.workflow_run.id }}, - }); - var matchArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "previewbuild" - })[0]; - var download = await github.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); - - name: Extract Artifacts - run: unzip -d webapp previewbuild.zip && rm previewbuild.zip - - name: Deploy to Netlify - id: netlify - uses: nwtgck/actions-netlify@v1.2 - with: - publish-dir: webapp - deploy-message: "Deploy from GitHub Actions" - # These don't work because we're in workflow_run - enable-pull-request-comment: false - enable-commit-comment: false - alias: pr${{ steps.readctx.outputs.prnumber }} - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - timeout-minutes: 1 - - name: Edit PR Description - uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - pull-request-number: ${{ steps.readctx.outputs.prnumber }} - description-message: | - Preview: ${{ steps.netlify.outputs.deploy-url }} - ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. + build: + runs-on: ubuntu-latest + if: > + ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + steps: + - name: "🔍 Read PR number" + id: readctx + # we need to find the PR number that corresponds to the branch, which we do by + # searching the GH API + # The workflow_run event includes a list of pull requests, but it doesn't get populated for + # forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run + run: | + head_branch='${{github.event.workflow_run.head_repository.owner.login}}:${{github.event.workflow_run.head_branch}}' + echo "head branch: $head_branch" + pulls_uri="https://api.github.com/repos/${{ github.repository }}/pulls?head=$(jq -Rr '@uri' <<<$head_branch)" + pr_number=$(curl -s -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' "$pulls_uri" | + jq -r '.[] | .number') + echo "PR number: $pr_number" + echo "::set-output name=prnumber::$pr_number" + # There's a 'download artifact' action but it hasn't been updated for the + # workflow_run action (https://github.com/actions/download-artifact/issues/60) + # so instead we get this mess: + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "previewbuild" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); + + - name: Extract Artifacts + run: unzip -d webapp previewbuild.zip && rm previewbuild.zip + + - name: Deploy to Netlify + id: netlify + uses: nwtgck/actions-netlify@v1.2 + with: + publish-dir: webapp + deploy-message: "Deploy from GitHub Actions" + # These don't work because we're in workflow_run + enable-pull-request-comment: false + enable-commit-comment: false + alias: pr${{ steps.readctx.outputs.prnumber }} + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + timeout-minutes: 1 + + - name: Edit PR Description + uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + pull-request-number: ${{ steps.readctx.outputs.prnumber }} + description-message: | + Preview: ${{ steps.netlify.outputs.deploy-url }} + ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index ef463784f38..c5c89905ced 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -1,15 +1,18 @@ name: Notify element-web on: - push: - branches: [develop] + push: + branches: [ develop ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - notify-element-web: - runs-on: ubuntu-latest - environment: develop - steps: - - name: Notify element-web repo that a new SDK build is on develop - uses: peter-evans/repository-dispatch@v1 - with: - token: ${{ secrets.ELEMENT_WEB_NOTIFY_TOKEN }} - repository: vector-im/element-web - event-type: element-web-notify + notify-element-web: + name: "Notify Element Web" + runs-on: ubuntu-latest + environment: develop + steps: + - name: Notify element-web repo that a new SDK build is on develop + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ secrets.ELEMENT_BOT_TOKEN }} + repository: vector-im/element-web + event-type: element-web-notify diff --git a/.github/workflows/preview_changelog.yaml b/.github/workflows/preview_changelog.yaml index d68d19361da..786d828d419 100644 --- a/.github/workflows/preview_changelog.yaml +++ b/.github/workflows/preview_changelog.yaml @@ -3,10 +3,10 @@ on: pull_request_target: types: [ opened, edited, labeled ] jobs: - changelog: - runs-on: ubuntu-latest - steps: - - name: Preview Changelog - uses: matrix-org/allchange@main - with: - ghToken: ${{ secrets.GITHUB_TOKEN }} + changelog: + runs-on: ubuntu-latest + steps: + - name: Preview Changelog + uses: matrix-org/allchange@main + with: + ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml new file mode 100644 index 00000000000..8e320d99920 --- /dev/null +++ b/.github/workflows/static_analysis.yaml @@ -0,0 +1,88 @@ +name: Static Analysis +on: + pull_request: { } + push: + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] +jobs: + ts_lint: + name: "Typescript Syntax Check" + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Install Deps + run: "./scripts/ci/install-deps.sh --ignore-scripts" + + - name: Typecheck + run: "yarn run lint:types" + + - name: Switch js-sdk to release mode + run: | + scripts/ci/js-sdk-to-release.js + cd node_modules/matrix-js-sdk + yarn install + yarn run build:compile + yarn run build:types + + - name: Typecheck (release mode) + run: "yarn run lint:types" + + i18n_lint: + name: "i18n Diff Check" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: i18n Check + run: "yarn run diff-i18n" + + js_lint: + name: "ESLint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: Run Linter + run: "yarn run lint:js" + + style_lint: + name: "Style Lint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: Run Linter + run: "yarn run lint:style" diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/tests.yml similarity index 78% rename from .github/workflows/test_coverage.yml rename to .github/workflows/tests.yml index 4cd9f6d2f06..dc11981b7cf 100644 --- a/.github/workflows/test_coverage.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,13 @@ -name: Test coverage +name: Tests on: - pull_request: {} + pull_request: { } push: - branches: [develop, main, master] + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - test-coverage: + jest: + name: Jest with Codecov runs-on: ubuntu-latest env: # This must be set for fetchdep.sh to get the right branch @@ -19,13 +22,15 @@ jobs: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} - name: Yarn cache - uses: c-hive/gha-yarn-cache@v2 + uses: actions/setup-node@v3 + with: + cache: 'yarn' - name: Install Deps run: "./scripts/ci/install-deps.sh --ignore-scripts" - name: Run tests with coverage - run: "yarn install && yarn coverage" + run: "yarn coverage" - name: Upload coverage uses: codecov/codecov-action@v2 diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/typecheck.yaml deleted file mode 100644 index 60cabb3caba..00000000000 --- a/.github/workflows/typecheck.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Type Check -on: - pull_request: - branches: [develop] -jobs: - build: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - steps: - - uses: actions/checkout@v2 - - uses: c-hive/gha-yarn-cache@v2 - - name: Install Deps - run: "./scripts/ci/install-deps.sh --ignore-scripts" - - name: Typecheck - run: "yarn run lint:types" - - name: Switch js-sdk to release mode - run: | - scripts/ci/js-sdk-to-release.js - cd node_modules/matrix-js-sdk - yarn install - yarn run build:compile - yarn run build:types - - name: Typecheck (release mode) - run: "yarn run lint:types" - From 6f5900557b888184b148cb2cf4c568762ded1032 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 15:17:43 +0100 Subject: [PATCH 04/10] Remove notify environment requirement (#8383) --- .github/workflows/notify-element-web.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index c5c89905ced..1d60a1523cc 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -8,7 +8,6 @@ jobs: notify-element-web: name: "Notify Element Web" runs-on: ubuntu-latest - environment: develop steps: - name: Notify element-web repo that a new SDK build is on develop uses: peter-evans/repository-dispatch@v1 From c70816d76322c62e21d96d2e914193a076fd9b3c Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 11:00:32 -0400 Subject: [PATCH 05/10] Persist audio and video mute state in video rooms (#8376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …so that video lobbies remember whether you've disabled your camera. --- src/components/views/voip/VideoLobby.tsx | 16 ++++++--- src/stores/VideoChannelStore.ts | 44 +++++++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx index f9e95089270..862e58fe630 100644 --- a/src/components/views/voip/VideoLobby.tsx +++ b/src/components/views/voip/VideoLobby.tsx @@ -21,7 +21,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../../languageHandler"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; -import { useStateToggle } from "../../../hooks/useStateToggle"; import { useConnectedMembers } from "../../../utils/VideoChannelUtils"; import VideoChannelStore from "../../../stores/VideoChannelStore"; import IconizedContextMenu, { @@ -108,6 +107,7 @@ const DeviceButton: FC = ({ const MAX_FACES = 8; const VideoLobby: FC<{ room: Room }> = ({ room }) => { + const store = VideoChannelStore.instance; const [connecting, setConnecting] = useState(false); const me = useMemo(() => room.getMember(room.myUserId), [room]); const connectedMembers = useConnectedMembers(room, false); @@ -130,8 +130,16 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { const audioDevice = selectedAudioDevice ?? audioDevices[0]; const videoDevice = selectedVideoDevice ?? videoDevices[0]; - const [audioActive, toggleAudio] = useStateToggle(true); - const [videoActive, toggleVideo] = useStateToggle(true); + const [audioActive, setAudioActive] = useState(!store.audioMuted); + const [videoActive, setVideoActive] = useState(!store.videoMuted); + const toggleAudio = () => { + store.audioMuted = audioActive; + setAudioActive(!audioActive); + }; + const toggleVideo = () => { + store.videoMuted = videoActive; + setVideoActive(!videoActive); + }; const videoStream = useAsyncMemo(async () => { if (videoDevice && videoActive) { @@ -162,7 +170,7 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { const connect = async () => { setConnecting(true); try { - await VideoChannelStore.instance.connect( + await store.connect( room.roomId, audioActive ? audioDevice : null, videoActive ? videoDevice : null, ); } catch (e) { diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 14016800487..d32f748fb7f 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -94,6 +94,20 @@ export default class VideoChannelStore extends AsyncStoreWithClient { public get participants(): IJitsiParticipant[] { return this._participants; } private set participants(value: IJitsiParticipant[]) { this._participants = value; } + private _audioMuted = localStorage.getItem("mx_audioMuted") === "true"; + public get audioMuted(): boolean { return this._audioMuted; } + public set audioMuted(value: boolean) { + this._audioMuted = value; + localStorage.setItem("mx_audioMuted", value.toString()); + } + + private _videoMuted = localStorage.getItem("mx_videoMuted") === "true"; + public get videoMuted(): boolean { return this._videoMuted; } + public set videoMuted(value: boolean) { + this._videoMuted = value; + localStorage.setItem("mx_videoMuted", value.toString()); + } + public connect = async (roomId: string, audioDevice: MediaDeviceInfo, videoDevice: MediaDeviceInfo) => { if (this.activeChannel) await this.disconnect(); @@ -136,10 +150,14 @@ export default class VideoChannelStore extends AsyncStoreWithClient { } } + // Participant data and mute state will come down the event pipeline quickly, so prepare in advance this.activeChannel = messaging; this.roomId = roomId; - // Participant data will come down the event pipeline quickly, so prepare in advance messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + messaging.on(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio); + messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio); + messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo); + messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo); this.emit(VideoChannelEvent.StartConnect, roomId); @@ -163,6 +181,10 @@ export default class VideoChannelStore extends AsyncStoreWithClient { this.activeChannel = null; this.roomId = null; messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio); + messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio); + messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo); + messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo); this.emit(VideoChannelEvent.Disconnect, roomId); @@ -238,4 +260,24 @@ export default class VideoChannelStore extends AsyncStoreWithClient { this.emit(VideoChannelEvent.Participants, this.roomId, ev.detail.data.participants); this.ack(ev); }; + + private onMuteAudio = (ev: CustomEvent) => { + this.audioMuted = true; + this.ack(ev); + }; + + private onUnmuteAudio = (ev: CustomEvent) => { + this.audioMuted = false; + this.ack(ev); + }; + + private onMuteVideo = (ev: CustomEvent) => { + this.videoMuted = true; + this.ack(ev); + }; + + private onUnmuteVideo = (ev: CustomEvent) => { + this.videoMuted = false; + this.ack(ev); + }; } From 73e8387799ba446a6da62e41c24390a5fbfa7b27 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 21 Apr 2022 18:14:10 +0200 Subject: [PATCH 06/10] test functional EventUtils (#8386) * extract poll event test util Signed-off-by: Kerry Archibald * test isContentActionable Signed-off-by: Kerry Archibald * test canEditContent Signed-off-by: Kerry Archibald * test functional eventutils Signed-off-by: Kerry Archibald * copyrights Signed-off-by: Kerry Archibald --- src/utils/EventUtils.ts | 2 +- .../previews/PollStartEventPreview-test.ts | 40 +- test/test-utils/index.ts | 1 + test/test-utils/poll.ts | 50 +++ test/utils/EventUtils-test.ts | 357 ++++++++++++++++++ 5 files changed, 413 insertions(+), 37 deletions(-) create mode 100644 test/test-utils/poll.ts create mode 100644 test/utils/EventUtils-test.ts diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 51ab6cbed06..0a860ccf013 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -82,7 +82,7 @@ export function canEditContent(mxEvent: MatrixEvent): boolean { M_POLL_START.matches(mxEvent.getType()) || ( (msgtype === MsgType.Text || msgtype === MsgType.Emote) && - body && + !!body && typeof body === 'string' ) ); diff --git a/test/stores/room-list/previews/PollStartEventPreview-test.ts b/test/stores/room-list/previews/PollStartEventPreview-test.ts index a7726167e11..b69e7da9767 100644 --- a/test/stores/room-list/previews/PollStartEventPreview-test.ts +++ b/test/stores/room-list/previews/PollStartEventPreview-test.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { POLL_ANSWER, M_TEXT, M_POLL_KIND_DISCLOSED, M_POLL_START } from "matrix-events-sdk"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { PollStartEventPreview } from "../../../../src/stores/room-list/previews/PollStartEventPreview"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { makePollStartEvent } from "../../../test-utils"; jest.spyOn(MatrixClientPeg, 'get').mockReturnValue({ getUserId: () => "@me:example.com", @@ -26,47 +26,15 @@ jest.spyOn(MatrixClientPeg, 'get').mockReturnValue({ describe("PollStartEventPreview", () => { it("shows the question for a poll I created", async () => { - const pollStartEvent = newPollStartEvent("My Question", "@me:example.com"); + const pollStartEvent = makePollStartEvent("My Question", "@me:example.com"); const preview = new PollStartEventPreview(); expect(preview.getTextFor(pollStartEvent)).toBe("My Question"); }); it("shows the sender and question for a poll created by someone else", async () => { - const pollStartEvent = newPollStartEvent("Your Question", "@yo:example.com"); + const pollStartEvent = makePollStartEvent("Your Question", "@yo:example.com"); const preview = new PollStartEventPreview(); expect(preview.getTextFor(pollStartEvent)).toBe("@yo:example.com: Your Question"); }); }); -function newPollStartEvent( - question: string, - sender: string, - answers?: POLL_ANSWER[], -): MatrixEvent { - if (!answers) { - answers = [ - { "id": "socks", [M_TEXT.name]: "Socks" }, - { "id": "shoes", [M_TEXT.name]: "Shoes" }, - ]; - } - - return new MatrixEvent( - { - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "sender": sender, - "type": M_POLL_START.name, - "content": { - [M_POLL_START.name]: { - "question": { - [M_TEXT.name]: question, - }, - "kind": M_POLL_KIND_DISCLOSED.name, - "answers": answers, - }, - [M_TEXT.name]: `${question}: answers`, - }, - }, - ); -} - diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index c886e6a798b..44ea28c9660 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -2,6 +2,7 @@ export * from './beacon'; export * from './client'; export * from './location'; export * from './platform'; +export * from './poll'; export * from './room'; export * from './test-utils'; export * from './video'; diff --git a/test/test-utils/poll.ts b/test/test-utils/poll.ts new file mode 100644 index 00000000000..ca25b9eaa0a --- /dev/null +++ b/test/test-utils/poll.ts @@ -0,0 +1,50 @@ +/* +Copyright 2022 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/matrix"; +import { M_TEXT, M_POLL_START, POLL_ANSWER, M_POLL_KIND_DISCLOSED } from "matrix-events-sdk"; + +export const makePollStartEvent = ( + question: string, + sender: string, + answers?: POLL_ANSWER[], +): MatrixEvent => { + if (!answers) { + answers = [ + { "id": "socks", [M_TEXT.name]: "Socks" }, + { "id": "shoes", [M_TEXT.name]: "Shoes" }, + ]; + } + + return new MatrixEvent( + { + "event_id": "$mypoll", + "room_id": "#myroom:example.com", + "sender": sender, + "type": M_POLL_START.name, + "content": { + [M_POLL_START.name]: { + "question": { + [M_TEXT.name]: question, + }, + "kind": M_POLL_KIND_DISCLOSED.name, + "answers": answers, + }, + [M_TEXT.name]: `${question}: answers`, + }, + }, + ); +}; diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts new file mode 100644 index 00000000000..674162f548d --- /dev/null +++ b/test/utils/EventUtils-test.ts @@ -0,0 +1,357 @@ +/* +Copyright 2022 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 { M_LOCATION } from "matrix-js-sdk/src/@types/location"; +import { + EventStatus, + EventType, + MatrixEvent, + MsgType, + RelationType, +} from "matrix-js-sdk/src/matrix"; + +import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import { + canCancel, + canEditContent, + canEditOwnEvent, + canForward, + isContentActionable, + isLocationEvent, + isVoiceMessage, +} from "../../src/utils/EventUtils"; +import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent } from "../test-utils"; + +describe('EventUtils', () => { + const userId = '@user:server'; + const roomId = '!room:server'; + const mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue(userId), + }); + + beforeEach(() => { + mockClient.getUserId.mockClear().mockReturnValue(userId); + }); + afterAll(() => { + jest.spyOn(MatrixClientPeg, 'get').mockRestore(); + }); + + // setup events + const unsentEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + }); + unsentEvent.status = EventStatus.ENCRYPTING; + + const redactedEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + }); + redactedEvent.makeRedacted(redactedEvent); + + const stateEvent = makeBeaconInfoEvent(userId, roomId); + + const roomMemberEvent = new MatrixEvent({ + type: EventType.RoomMember, + sender: userId, + }); + + const stickerEvent = new MatrixEvent({ + type: EventType.Sticker, + sender: userId, + }); + + const pollStartEvent = makePollStartEvent('What?', userId); + + const notDecryptedEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: 'm.bad.encrypted', + }, + }); + + const noMsgType = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: undefined, + }, + }); + + const noContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Image, + }, + }); + + const emptyContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: '', + }, + }); + + const objectContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.File, + body: {}, + }, + }); + + const niceTextMessage = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + + const bobsTextMessage = new MatrixEvent({ + type: EventType.RoomMessage, + sender: '@bob:server', + content: { + msgtype: MsgType.Text, + body: 'Hello from Bob', + }, + }); + + describe('isContentActionable()', () => { + type TestCase = [string, MatrixEvent]; + it.each([ + ['unsent event', unsentEvent], + ['redacted event', redactedEvent], + ['state event', stateEvent], + ['undecrypted event', notDecryptedEvent], + ['room member event', roomMemberEvent], + ['event without msgtype', noMsgType], + ['event without content body property', noContentBody], + ])('returns false for %s', (_description, event) => { + expect(isContentActionable(event)).toBe(false); + }); + + it.each([ + ['sticker event', stickerEvent], + ['poll start event', pollStartEvent], + ['event with empty content body', emptyContentBody], + ['event with a content body', niceTextMessage], + ])('returns true for %s', (_description, event) => { + expect(isContentActionable(event)).toBe(true); + }); + }); + + describe('editable content helpers', () => { + const replaceRelationEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + ['m.relates_to']: { + rel_type: RelationType.Replace, + event_id: '1', + }, + }, + }); + + const referenceRelationEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + ['m.relates_to']: { + rel_type: RelationType.Reference, + event_id: '1', + }, + }, + }); + + const emoteEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Emote, + body: '🧪', + }, + }); + + type TestCase = [string, MatrixEvent]; + + const uneditableCases: TestCase[] = [ + ['redacted event', redactedEvent], + ['state event', stateEvent], + ['event that is not room message', roomMemberEvent], + ['event without msgtype', noMsgType], + ['event without content body property', noContentBody], + ['event with empty content body property', emptyContentBody], + ['event with non-string body', objectContentBody], + ['event not sent by current user', bobsTextMessage], + ['event with a replace relation', replaceRelationEvent], + ]; + + const editableCases: TestCase[] = [ + ['event with reference relation', referenceRelationEvent], + ['emote event', emoteEvent], + ['poll start event', pollStartEvent], + ['event with a content body', niceTextMessage], + ]; + + describe('canEditContent()', () => { + it.each(uneditableCases)('returns false for %s', (_description, event) => { + expect(canEditContent(event)).toBe(false); + }); + + it.each(editableCases)('returns true for %s', (_description, event) => { + expect(canEditContent(event)).toBe(true); + }); + }); + describe('canEditOwnContent()', () => { + it.each(uneditableCases)('returns false for %s', (_description, event) => { + expect(canEditOwnEvent(event)).toBe(false); + }); + + it.each(editableCases)('returns true for %s', (_description, event) => { + expect(canEditOwnEvent(event)).toBe(true); + }); + }); + }); + + describe('isVoiceMessage()', () => { + it('returns true for an event with msc2516.voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + ['org.matrix.msc2516.voice']: {}, + }, + }); + + expect(isVoiceMessage(event)).toBe(true); + }); + + it('returns true for an event with msc3245.voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + ['org.matrix.msc3245.voice']: {}, + }, + }); + + expect(isVoiceMessage(event)).toBe(true); + }); + + it('returns false for an event with voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'hello', + }, + }); + + expect(isVoiceMessage(event)).toBe(false); + }); + }); + + describe('isLocationEvent()', () => { + it('returns true for an event with m.location stable type', () => { + const event = new MatrixEvent({ + type: M_LOCATION.altName, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for an event with m.location unstable prefixed type', () => { + const event = new MatrixEvent({ + type: M_LOCATION.name, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for a room message with stable m.location msgtype', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + msgtype: M_LOCATION.altName, + }, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for a room message with unstable m.location msgtype', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + msgtype: M_LOCATION.name, + }, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns false for a non location event', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'Hello', + }, + }); + expect(isLocationEvent(event)).toBe(false); + }); + }); + + describe('canForward()', () => { + it('returns false for a location event', () => { + const event = new MatrixEvent({ + type: M_LOCATION.name, + }); + expect(canForward(event)).toBe(false); + }); + it('returns false for a poll event', () => { + const event = makePollStartEvent('Who?', userId); + expect(canForward(event)).toBe(false); + }); + it('returns true for a room message event', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'Hello', + }, + }); + expect(canForward(event)).toBe(true); + }); + }); + + describe('canCancel()', () => { + it.each([ + [EventStatus.QUEUED], + [EventStatus.NOT_SENT], + [EventStatus.ENCRYPTING], + ])('return true for status %s', (status) => { + expect(canCancel(status)).toBe(true); + }); + + it.each([ + [EventStatus.SENDING], + [EventStatus.CANCELLED], + [EventStatus.SENT], + ['invalid-status' as unknown as EventStatus], + ])('return false for status %s', (status) => { + expect(canCancel(status)).toBe(false); + }); + }); +}); From 86419b19255ac1af06da6a17666d29bf267750d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 17:21:44 +0100 Subject: [PATCH 07/10] Fix regression around the room list treeview keyboard a11y (#8385) --- src/components/views/rooms/RoomList.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index e2550bc2b6e..9251cfc2ba6 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -616,11 +616,8 @@ export default class RoomList extends React.PureComponent { public focus(): void { // focus the first focusable element in this aria treeview widget const treeItems = this.treeRef.current?.querySelectorAll('[role="treeitem"]'); - if (treeItems) { - return; - } - [...treeItems] - .find(e => e.offsetParent !== null)?.focus(); + if (!treeItems) return; + [...treeItems].find(e => e.offsetParent !== null)?.focus(); } public render() { From 399ac618c767d47f035d75a59d22b6db93459f10 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 21 Apr 2022 18:56:11 +0200 Subject: [PATCH 08/10] LLS: fix jumpy maximised map (#8387) * add maxzoom to map fit bounds Signed-off-by: Kerry Archibald * take snapshot of bounds at center on dialog open Signed-off-by: Kerry Archibald --- .../views/beacon/BeaconViewDialog.tsx | 18 ++++++++--- src/components/views/location/Map.tsx | 2 +- .../views/beacon/BeaconViewDialog-test.tsx | 30 +++++++++++++++++++ test/components/views/location/Map-test.tsx | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 9dc1352f105..e6c4a423fe9 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { Beacon, @@ -56,6 +56,17 @@ const getBoundsCenter = (bounds: Bounds): string | undefined => { }); }; +const useInitialMapPosition = (liveBeacons: Beacon[], focusBeacon?: Beacon): { + bounds?: Bounds; centerGeoUri: string; +} => { + const bounds = useRef(getBeaconBounds(liveBeacons)); + const centerGeoUri = useRef( + focusBeacon?.latestLocationState?.uri || + getBoundsCenter(bounds.current), + ); + return { bounds: bounds.current, centerGeoUri: centerGeoUri.current }; +}; + /** * Dialog to view live beacons maximised */ @@ -69,8 +80,7 @@ const BeaconViewDialog: React.FC = ({ const [isSidebarOpen, setSidebarOpen] = useState(false); - const bounds = getBeaconBounds(liveBeacons); - const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds); + const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon); return ( = ({ fixedWidth={false} > - { !!bounds ? [bounds.west, bounds.south], [bounds.east, bounds.north], ); - map.fitBounds(lngLatBounds, { padding: 100 }); + map.fitBounds(lngLatBounds, { padding: 100, maxZoom: 15 }); } catch (error) { logger.error('Invalid map bounds', error); } diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx index 0f23036b8b1..8d0fb30e307 100644 --- a/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -24,6 +24,7 @@ import { RoomMember, getBeaconInfoIdentifier, } from 'matrix-js-sdk/src/matrix'; +import maplibregl from 'maplibre-gl'; import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog'; import { @@ -58,6 +59,8 @@ describe('', () => { getVisibleRooms: jest.fn().mockReturnValue([]), }); + const mockMap = new maplibregl.Map(); + // make fresh rooms every time // as we update room state const setupRoom = (stateEvents: MatrixEvent[] = []): Room => { @@ -88,6 +91,8 @@ describe('', () => { beforeEach(() => { jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore(); + + jest.clearAllMocks(); }); it('renders a map with markers', () => { @@ -151,6 +156,31 @@ describe('', () => { expect(component.find('BeaconMarker').length).toEqual(2); }); + it('does not update bounds or center on changing beacons', () => { + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + expect(component.find('BeaconMarker').length).toEqual(1); + + const anotherBeaconEvent = makeBeaconInfoEvent(bobId, + roomId, + { isLive: true }, + '$bob-room1-1', + ); + + act(() => { + // emits RoomStateEvent.BeaconLiveness + room.currentState.setStateEvents([anotherBeaconEvent]); + }); + + component.setProps({}); + + // two markers now! + expect(mockMap.setCenter).toHaveBeenCalledTimes(1); + expect(mockMap.fitBounds).toHaveBeenCalledTimes(1); + }); + it('renders a fallback when no live beacons remain', () => { const onFinished = jest.fn(); const room = setupRoom([defaultEvent]); diff --git a/test/components/views/location/Map-test.tsx b/test/components/views/location/Map-test.tsx index a1e1680b18a..72826a9cdf2 100644 --- a/test/components/views/location/Map-test.tsx +++ b/test/components/views/location/Map-test.tsx @@ -125,7 +125,7 @@ describe('', () => { const bounds = { north: 51, south: 50, east: 42, west: 41 }; getComponent({ bounds }); expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds([bounds.west, bounds.south], - [bounds.east, bounds.north]), { padding: 100 }); + [bounds.east, bounds.north]), { padding: 100, maxZoom: 15 }); }); it('handles invalid bounds', () => { From bbe0c945d3c0ad6a92ec7ca47b9273d36c32564c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 17:57:08 +0100 Subject: [PATCH 09/10] Fix regression around haveRendererForEvent for hidden events (#8379) --- src/components/structures/MessagePanel.tsx | 8 ---- src/events/EventTileFactory.tsx | 2 + .../structures/MessagePanel-test.js | 39 ++++++++++++++++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index ff9bc826528..8600da90475 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -452,14 +452,6 @@ export default class MessagePanel extends React.Component { } } - /* check the scroll state and send out pagination requests if necessary. - */ - public checkFillState(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.checkFillState(); - } - } - private isUnmounting = (): boolean => { return !this.isMounted; }; diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 7beda8cc305..ecda002f591 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -393,6 +393,8 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo return hasText(mxEvent, showHiddenEvents); } else if (handler === STATE_EVENT_TILE_TYPES[EventType.RoomCreate]) { return Boolean(mxEvent.getContent()['predecessor']); + } else if (handler === JSONEventFactory) { + return false; } else { return true; } diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 3c774290005..eca8c39bec9 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -44,6 +44,8 @@ class WrappedMessagePanel extends React.Component { callEventGroupers = new Map(); render() { + const { showHiddenEvents, ...props } = this.props; + const roomContext = { room, roomId: room.roomId, @@ -54,13 +56,14 @@ class WrappedMessagePanel extends React.Component { showJoinLeaves: false, showAvatarChanges: false, showDisplaynameChanges: true, + showHiddenEvents, }; return @@ -633,6 +636,40 @@ describe('MessagePanel', function() { expect(settingsSpy).not.toHaveBeenCalledWith("showHiddenEventsInTimeline"); settingsSpy.mockRestore(); }); + + it("should group hidden event reactions into an event list summary", () => { + const events = [ + TestUtilsMatrix.mkEvent({ + event: true, + type: "m.reaction", + room: "!room:id", + user: "@user:id", + content: {}, + ts: 1, + }), + TestUtilsMatrix.mkEvent({ + event: true, + type: "m.reaction", + room: "!room:id", + user: "@user:id", + content: {}, + ts: 2, + }), + TestUtilsMatrix.mkEvent({ + event: true, + type: "m.reaction", + room: "!room:id", + user: "@user:id", + content: {}, + ts: 3, + }), + ]; + const res = mount(); + + const els = res.find("EventListSummary"); + expect(els.length).toEqual(1); + expect(els.prop("events").length).toEqual(3); + }); }); describe("shouldFormContinuation", () => { From 495a69532d4ddc2fe3e10fe8260869bdb68f99e9 Mon Sep 17 00:00:00 2001 From: Yaya Usman <38439166+yaya-usman@users.noreply.github.com> Date: Fri, 22 Apr 2022 00:13:39 +0300 Subject: [PATCH 10/10] fix: "Mention highlight and cursor hover highlight has different corner radius" (#8384) * fix: mention tile and hover highlight's same border radius * style-lint fix * update: mention tile and hover highlight's same border radius --- res/css/views/rooms/_EventTile.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index f28fa3b71d4..497de258dc5 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -223,12 +223,6 @@ $left-gutter: 64px; overflow-y: hidden; } - &.mx_EventTile_selected .mx_EventTile_line, - &:hover .mx_EventTile_line { - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - } - &:hover.mx_EventTile_verified .mx_EventTile_line { box-shadow: inset calc(50px + $selected-message-border-width) 0 0 -50px $e2e-verified-color; }