From b12a2723853fb615e8e50b2a9bf2b6a191570fc4 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Tue, 31 May 2022 21:52:12 +0000 Subject: [PATCH 001/183] Use AccessibleButton for 'In reply to' link button on ReplyChain (#8726) - Remove ButtonResetDefault mixin to respect the concept of cascading Signed-off-by: Suguru Hirahara --- res/css/views/elements/_ReplyChain.scss | 17 ++++++++--------- src/components/views/elements/ReplyChain.tsx | 10 +++++++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/res/css/views/elements/_ReplyChain.scss b/res/css/views/elements/_ReplyChain.scss index cb3c0787712..a949feaf739 100644 --- a/res/css/views/elements/_ReplyChain.scss +++ b/res/css/views/elements/_ReplyChain.scss @@ -15,20 +15,19 @@ limitations under the License. */ .mx_ReplyChain { - margin-top: 0; - margin-left: 0; - margin-right: 0; - margin-bottom: 8px; - padding: 0 10px; + margin: 0 0 $spacing-8 0; + padding: 0 10px; // TODO: Use a spacing variable border-left: 2px solid $accent; border-radius: 2px; .mx_ReplyChain_show { - @mixin ButtonResetDefault; - color: inherit; + &.mx_AccessibleButton_kind_link_inline { + padding: 0; + color: unset; - &:hover { - color: $links; + &:hover { + color: $links; + } } } diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index dd7cf768556..8fd0217d7ce 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -32,7 +32,7 @@ import { Action } from "../../../dispatcher/actions"; import Spinner from './Spinner'; import ReplyTile from "../rooms/ReplyTile"; import Pill, { PillType } from './Pill'; -import { ButtonEvent } from './AccessibleButton'; +import AccessibleButton, { ButtonEvent } from './AccessibleButton'; import { getParentEventId, shouldDisplayReply } from '../../../utils/Reply'; import RoomContext from "../../../contexts/RoomContext"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -217,9 +217,13 @@ export default class ReplyChain extends React.Component { { _t('In reply to ', {}, { 'a': (sub) => ( - + ), 'pill': ( Date: Wed, 1 Jun 2022 03:51:11 +0530 Subject: [PATCH 002/183] Change dash to em dash issues fixed (#8455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * addedd em dash * Update src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx Co-authored-by: Šimon Brandner * changes pushed Co-authored-by: Šimon Brandner Co-authored-by: Travis Ralston --- .../views/settings/tabs/user/GeneralUserSettingsTab.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index e0874084251..3c7bd833b73 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -427,7 +427,7 @@ export default class GeneralUserSettingsTab extends React.Component { _t("Account management") } - { _t("Deactivating your account is a permanent action - be careful!") } + { _t("Deactivating your account is a permanent action — be careful!") } { _t("Deactivate Account") } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3aa40c62e6e..71157585d9e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1437,7 +1437,7 @@ "Spell check dictionaries": "Spell check dictionaries", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", "Account management": "Account management", - "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", + "Deactivating your account is a permanent action — be careful!": "Deactivating your account is a permanent action — be careful!", "Deactivate Account": "Deactivate Account", "Deactivate account": "Deactivate account", "Discovery": "Discovery", From 23cc1aff73795c90d0807af7f331d02f1743121d Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Wed, 1 Jun 2022 04:57:53 -0400 Subject: [PATCH 003/183] Fix flakiness of cypress crypto tests (#8731) --- cypress/integration/7-crypto/crypto.spec.ts | 24 ++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/cypress/integration/7-crypto/crypto.spec.ts b/cypress/integration/7-crypto/crypto.spec.ts index 2446e3bd2bb..6f1f7aa6c89 100644 --- a/cypress/integration/7-crypto/crypto.spec.ts +++ b/cypress/integration/7-crypto/crypto.spec.ts @@ -19,13 +19,17 @@ limitations under the License. import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import { SynapseInstance } from "../../plugins/synapsedocker"; -function waitForEncryption(cli: MatrixClient, roomId: string, win: Cypress.AUTWindow, resolve: () => void) { - cli.crypto.cryptoStore.getEndToEndRooms(null, (result) => { - if (result[roomId]) { - resolve(); - } else { - cli.once(win.matrixcs.RoomStateEvent.Update, () => waitForEncryption(cli, roomId, win, resolve)); - } +function waitForEncryption(cli: MatrixClient, roomId: string, win: Cypress.AUTWindow): Promise { + return new Promise(resolve => { + const onEvent = () => { + cli.crypto.cryptoStore.getEndToEndRooms(null, (result) => { + if (result[roomId]) { + cli.off(win.matrixcs.ClientEvent.Event, onEvent); + resolve(); + } + }); + }; + cli.on(win.matrixcs.ClientEvent.Event, onEvent); }); } @@ -61,15 +65,15 @@ describe("Cryptography", () => { cy.window(), ]).then(([bot, roomId, win]) => { cy.inviteUser(roomId, bot.getUserId()); - cy.visit("/#/room/" + roomId); cy.wrap( - new Promise(resolve => - waitForEncryption(bot, roomId, win, resolve), + waitForEncryption( + bot, roomId, win, ).then(() => bot.sendMessage(roomId, { body: "Top secret message", msgtype: "m.text", })), ); + cy.visit("/#/room/" + roomId); }); cy.get(".mx_RoomView_body .mx_cryptoEvent").should("contain", "Encryption enabled"); From 6574c5c3e2835c6cf25bed58e9e0b62a188dcf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Jun 2022 17:14:43 +0200 Subject: [PATCH 004/183] Fix `CallView` crash (#8735) --- src/components/views/voip/CallView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 296ebd79ae5..b212a2d5ba8 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -418,7 +418,8 @@ export default class CallView extends React.Component { const isScreensharing = call.isScreensharing(); const { primaryFeed, sidebarShown } = this.state; - const sharerName = primaryFeed.getMember().name; + const sharerName = primaryFeed?.getMember().name; + if (!sharerName) return; let text = isScreensharing ? _t("You are presenting") From 7c57680b93db3afeb9f52a65a5302d66d79a56b9 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Jun 2022 02:43:07 +0000 Subject: [PATCH 005/183] Fix read avatars overflow from the right chat panel with a maximized widget on bubble message layout (#8470) * Fix RR overflow on the right chat panel Signed-off-by: Suguru Hirahara * Align with RR outside of info tile Signed-off-by: Suguru Hirahara * Use inset-inline property Signed-off-by: Suguru Hirahara --- res/css/views/right_panel/_TimelineCard.scss | 21 ++++++++++++++------ res/css/views/rooms/_EventBubbleTile.scss | 11 ++++++---- res/css/views/rooms/_ReadReceiptGroup.scss | 2 ++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index 3bc71040811..e2711b8c594 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -92,7 +92,8 @@ limitations under the License. } .mx_EventTile_msgOption { - margin-right: 2px; + // Override mx_EventTile_msgOption of mx_EventTile:not([data-layout="bubble"]) + margin-inline-end: 0; } &.mx_EventTile_info { @@ -149,11 +150,19 @@ limitations under the License. flex-basis: 48px; // 12 (padding on message list) + 36 (padding on event lines) } - &.mx_BaseCard { - // For a chat timeline on the right panel when the widget is maximised - // TODO: rename ThreadPanel - &.mx_ThreadPanel { - padding-right: 8px; // .mx_RightPanel padding + .mx_GenericEventListSummary_unstyledList, // RR next to a message on the event list summary + .mx_RoomView_MessageList { // RR next to a message on the messsge list + .mx_EventTile[data-layout=bubble] { + .mx_ReadReceiptGroup { + // 6px: scroll bar width (magic number) + inset-inline-end: calc(-1 * var(--ReadReceiptGroup_EventBubbleTile-spacing-end) + $container-gap-width + 6px); + } + + &.mx_EventTile_info { + .mx_ReadReceiptGroup { + inset-inline-end: -4px; // align with RR outside of info tile + } + } } } } diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 75071366fac..c6a6c276c8d 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -539,8 +539,10 @@ limitations under the License. .mx_ReadReceiptGroup { position: absolute; + // as close to right gutter without clipping as possible - right: -78px; + inset-inline-end: calc(-1 * var(--ReadReceiptGroup_EventBubbleTile-spacing-end)); + // (EventTileLine.line-height - ReadReceiptGroup.height) / 2 // this centers the ReadReceiptGroup if we’ve got a single line bottom: calc(($font-18px - 24px) / 2); @@ -691,15 +693,16 @@ limitations under the License. .mx_MessageActionBar { inset-inline-start: initial; // Reset .mx_EventTile[data-layout="bubble"][data-self="false"] .mx_MessageActionBar - right: 48px; // align with that of right-column bubbles + inset-inline-end: 48px; // align with that of right-column bubbles } .mx_ReadReceiptGroup { - right: -18px; // match alignment to RRs of chat bubbles + // match alignment to RRs of chat bubbles + inset-inline-end: calc(-1 * var(--ReadReceiptGroup_EventBubbleTile-spacing-end) + 60px); } &::before { - right: 0; // match alignment of the hover background to that of chat bubbles + inset-inline-end: 0; // match alignment of the hover background to that of chat bubbles } } } diff --git a/res/css/views/rooms/_ReadReceiptGroup.scss b/res/css/views/rooms/_ReadReceiptGroup.scss index fe40b1263f3..33f47506400 100644 --- a/res/css/views/rooms/_ReadReceiptGroup.scss +++ b/res/css/views/rooms/_ReadReceiptGroup.scss @@ -15,6 +15,8 @@ limitations under the License. */ .mx_ReadReceiptGroup { + --ReadReceiptGroup_EventBubbleTile-spacing-end: 78px; + position: relative; display: inline-block; // This aligns the avatar with the last line of the From 158e42f764103d4c883dc3306976dad4678d881f Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 2 Jun 2022 10:25:56 +0200 Subject: [PATCH 006/183] Unit test MessageActionBar (#8732) * test most basic paths in messageactionbar Signed-off-by: Kerry Archibald * tidy Signed-off-by: Kerry Archibald * add rtl * add code style note about using rtl Signed-off-by: Kerry Archibald * downgrade to rtl 12 * use rtl for MessageActionBar test Signed-off-by: Kerry Archibald * try mocking settingsstore for ci only failure Signed-off-by: Kerry Archibald * mock setValue too Signed-off-by: Kerry Archibald * uupdate lockfile Signed-off-by: Kerry Archibald --- code_style.md | 4 + package.json | 1 + .../context_menus/MessageContextMenu.tsx | 1 + .../views/emojipicker/ReactionPicker.tsx | 1 + .../views/messages/MessageActionBar-test.tsx | 363 ++++++++++++++++++ test/test-utils/client.ts | 13 + test/test-utils/utilities.ts | 1 + yarn.lock | 54 ++- 8 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 test/components/views/messages/MessageActionBar-test.tsx diff --git a/code_style.md b/code_style.md index 5747540a766..8071cd264bd 100644 --- a/code_style.md +++ b/code_style.md @@ -208,3 +208,7 @@ React information in component state that could be derived from the model? - Avoid things marked as Legacy or Deprecated in React 16 (e.g string refs and legacy contexts) + +Unit tests +----- +- New tests should use [react testing library](https://testing-library.com/docs/react-testing-library/intro/) \ No newline at end of file diff --git a/package.json b/package.json index fba76e4cec3..08223ace12f 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@babel/runtime": "^7.12.5", "@sentry/browser": "^6.11.0", "@sentry/tracing": "^6.11.0", + "@testing-library/react": "^12.1.5", "@types/geojson": "^7946.0.8", "await-lock": "^2.1.0", "blurhash": "^1.1.3", diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 8372ca14bfb..6703c34198a 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -670,6 +670,7 @@ export default class MessageContextMenu extends React.Component {...this.props} className="mx_MessageContextMenu" compact={true} + data-testid="mx_MessageContextMenu" > { nativeItemsList } { quickItemsList } diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index c53f5a64f9b..4a0d15b7292 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -124,6 +124,7 @@ class ReactionPicker extends React.Component { onChoose={this.onChoose} selectedEmojis={this.state.selectedEmojis} showQuickReactions={true} + data-testid='mx_ReactionPicker' />; } } diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx new file mode 100644 index 00000000000..3d624187b8c --- /dev/null +++ b/test/components/views/messages/MessageActionBar-test.tsx @@ -0,0 +1,363 @@ +/* +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 React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { act } from 'react-test-renderer'; +import { + EventType, + EventStatus, + MatrixEvent, + MatrixEventEvent, + MsgType, + Room, +} from 'matrix-js-sdk/src/matrix'; + +import MessageActionBar from '../../../../src/components/views/messages/MessageActionBar'; +import { + getMockClientWithEventEmitter, + mockClientMethodsUser, + mockClientMethodsEvents, +} from '../../../test-utils'; +import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; +import RoomContext, { TimelineRenderingType } from '../../../../src/contexts/RoomContext'; +import { IRoomState } from '../../../../src/components/structures/RoomView'; +import dispatcher from '../../../../src/dispatcher/dispatcher'; +import SettingsStore from '../../../../src/settings/SettingsStore'; + +jest.mock('../../../../src/dispatcher/dispatcher'); + +describe('', () => { + const userId = '@alice:server.org'; + const roomId = '!room:server.org'; + const alicesMessageEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + + const bobsMessageEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: '@bob:server.org', + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'I am bob', + }, + }); + + const redactedEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + }); + redactedEvent.makeRedacted(redactedEvent); + + const client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsEvents(), + getRoom: jest.fn(), + }); + const room = new Room(roomId, client, userId); + jest.spyOn(room, 'getPendingEvents').mockReturnValue([]); + + client.getRoom.mockReturnValue(room); + + const defaultProps = { + getTile: jest.fn(), + getReplyChain: jest.fn(), + toggleThreadExpanded: jest.fn(), + mxEvent: alicesMessageEvent, + permalinkCreator: new RoomPermalinkCreator(room), + }; + const defaultRoomContext = { + ...RoomContext, + timelineRenderingType: TimelineRenderingType.Room, + canSendMessages: true, + canReact: true, + } as unknown as IRoomState; + const getComponent = (props = {}, roomContext: Partial = {}) => + render( + + + ); + + beforeEach(() => { + jest.clearAllMocks(); + alicesMessageEvent.setStatus(EventStatus.SENT); + jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + jest.spyOn(SettingsStore, 'setValue').mockResolvedValue(undefined); + }); + + afterAll(() => { + jest.spyOn(SettingsStore, 'getValue').mockRestore(); + jest.spyOn(SettingsStore, 'setValue').mockRestore(); + }); + + it('kills event listeners on unmount', () => { + const offSpy = jest.spyOn(alicesMessageEvent, 'off').mockClear(); + const wrapper = getComponent({ mxEvent: alicesMessageEvent }); + + act(() => { + wrapper.unmount(); + }); + + expect(offSpy.mock.calls[0][0]).toEqual(MatrixEventEvent.Status); + expect(offSpy.mock.calls[1][0]).toEqual(MatrixEventEvent.Decrypted); + expect(offSpy.mock.calls[2][0]).toEqual(MatrixEventEvent.BeforeRedaction); + + expect(client.decryptEventIfNeeded).toHaveBeenCalled(); + }); + + describe('decryption', () => { + it('decrypts event if needed', () => { + getComponent({ mxEvent: alicesMessageEvent }); + expect(client.decryptEventIfNeeded).toHaveBeenCalled(); + }); + + it('updates component on decrypted event', () => { + const decryptingEvent = new MatrixEvent({ + type: EventType.RoomMessageEncrypted, + sender: userId, + room_id: roomId, + content: {}, + }); + jest.spyOn(decryptingEvent, 'isBeingDecrypted').mockReturnValue(true); + const { queryByLabelText } = getComponent({ mxEvent: decryptingEvent }); + + // still encrypted event is not actionable => no reply button + expect(queryByLabelText('Reply')).toBeFalsy(); + + act(() => { + // ''decrypt'' the event + decryptingEvent.event.type = alicesMessageEvent.getType(); + decryptingEvent.event.content = alicesMessageEvent.getContent(); + decryptingEvent.emit(MatrixEventEvent.Decrypted, decryptingEvent); + }); + + // new available actions after decryption + expect(queryByLabelText('Reply')).toBeTruthy(); + }); + }); + + describe('status', () => { + it('updates component when event status changes', () => { + alicesMessageEvent.setStatus(EventStatus.QUEUED); + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + + // pending event status, cancel action available + expect(queryByLabelText('Delete')).toBeTruthy(); + + act(() => { + alicesMessageEvent.setStatus(EventStatus.SENT); + }); + + // event is sent, no longer cancelable + expect(queryByLabelText('Delete')).toBeFalsy(); + }); + }); + + describe('redaction', () => { + // this doesn't do what it's supposed to + // because beforeRedaction event is fired... before redaction + // event is unchanged at point when this component updates + // TODO file bug + xit('updates component on before redaction event', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + const { queryByLabelText } = getComponent({ mxEvent: event }); + + // no pending redaction => no delete button + expect(queryByLabelText('Delete')).toBeFalsy(); + + act(() => { + const redactionEvent = new MatrixEvent({ + type: EventType.RoomRedaction, + sender: userId, + room_id: roomId, + }); + redactionEvent.setStatus(EventStatus.QUEUED); + event.markLocallyRedacted(redactionEvent); + }); + + // updated with local redaction event, delete now available + expect(queryByLabelText('Delete')).toBeTruthy(); + }); + }); + + describe('options button', () => { + it('renders options menu', () => { + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('Options')).toBeTruthy(); + }); + + it('opens message context menu on click', () => { + const { findByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + act(() => { + fireEvent.click(queryByLabelText('Options')); + }); + expect(findByTestId('mx_MessageContextMenu')).toBeTruthy(); + }); + }); + + describe('reply button', () => { + it('renders reply button on own actionable event', () => { + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('Reply')).toBeTruthy(); + }); + + it('renders reply button on others actionable event', () => { + const { queryByLabelText } = getComponent({ mxEvent: bobsMessageEvent }, { canSendMessages: true }); + expect(queryByLabelText('Reply')).toBeTruthy(); + }); + + it('does not render reply button on non-actionable event', () => { + // redacted event is not actionable + const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }); + expect(queryByLabelText('Reply')).toBeFalsy(); + }); + + it('does not render reply button when user cannot send messaged', () => { + // redacted event is not actionable + const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }, { canSendMessages: false }); + expect(queryByLabelText('Reply')).toBeFalsy(); + }); + + it('dispatches reply event on click', () => { + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + + act(() => { + fireEvent.click(queryByLabelText('Reply')); + }); + + expect(dispatcher.dispatch).toHaveBeenCalledWith({ + action: 'reply_to_event', + event: alicesMessageEvent, + context: TimelineRenderingType.Room, + }); + }); + }); + + describe('react button', () => { + it('renders react button on own actionable event', () => { + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('React')).toBeTruthy(); + }); + + it('renders react button on others actionable event', () => { + const { queryByLabelText } = getComponent({ mxEvent: bobsMessageEvent }); + expect(queryByLabelText('React')).toBeTruthy(); + }); + + it('does not render react button on non-actionable event', () => { + // redacted event is not actionable + const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }); + expect(queryByLabelText('React')).toBeFalsy(); + }); + + it('does not render react button when user cannot react', () => { + // redacted event is not actionable + const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }, { canReact: false }); + expect(queryByLabelText('React')).toBeFalsy(); + }); + + it('opens reaction picker on click', () => { + const { queryByLabelText, findByTestId } = getComponent({ mxEvent: alicesMessageEvent }); + act(() => { + fireEvent.click(queryByLabelText('React')); + }); + expect(findByTestId('mx_ReactionPicker')).toBeTruthy(); + }); + }); + + describe('cancel button', () => { + it('renders cancel button for an event with a cancelable status', () => { + alicesMessageEvent.setStatus(EventStatus.QUEUED); + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('Delete')).toBeTruthy(); + }); + + it('renders cancel button for an event with a pending edit', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + event.setStatus(EventStatus.SENT); + const replacingEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'replacing event body', + }, + }); + replacingEvent.setStatus(EventStatus.QUEUED); + event.makeReplaced(replacingEvent); + const { queryByLabelText } = getComponent({ mxEvent: event }); + expect(queryByLabelText('Delete')).toBeTruthy(); + }); + + it('renders cancel button for an event with a pending redaction', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + event.setStatus(EventStatus.SENT); + + const redactionEvent = new MatrixEvent({ + type: EventType.RoomRedaction, + sender: userId, + room_id: roomId, + }); + redactionEvent.setStatus(EventStatus.QUEUED); + + event.markLocallyRedacted(redactionEvent); + const { queryByLabelText } = getComponent({ mxEvent: event }); + expect(queryByLabelText('Delete')).toBeTruthy(); + }); + + it('renders cancel and retry button for an event with NOT_SENT status', () => { + alicesMessageEvent.setStatus(EventStatus.NOT_SENT); + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('Retry')).toBeTruthy(); + expect(queryByLabelText('Delete')).toBeTruthy(); + }); + + it.todo('unsends event on cancel click'); + it.todo('retrys event on retry click'); + }); +}); diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index ed4cabc501c..b922048837e 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -67,4 +67,17 @@ export const mockClientMethodsUser = (userId = '@alice:domain') => ({ getUserId: jest.fn().mockReturnValue(userId), isGuest: jest.fn().mockReturnValue(false), mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'), + credentials: { userId }, +}); + +/** + * Returns basic mocked client methods related to rendering events + * ``` + * const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser('@mytestuser:domain'), + }); + * ``` + */ +export const mockClientMethodsEvents = () => ({ + decryptEventIfNeeded: jest.fn(), }); diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index d7da56407b9..666f7c68ea6 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -38,6 +38,7 @@ export function untilDispatch(waitForAction: DispatcherAction): Promise (component: ReactWrapper, value: string) => component.find(`[${attr}="${value}"]`); export const findByTestId = findByAttr('data-test-id'); export const findById = findByAttr('id'); +export const findByAriaLabel = findByAttr('aria-label'); const findByTagAndAttr = (attr: string) => (component: ReactWrapper, value: string, tag: string) => diff --git a/yarn.lock b/yarn.lock index c212c635149..7c0d099633f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,7 +50,7 @@ "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" chokidar "^3.4.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== @@ -1852,11 +1852,39 @@ remark "^13.0.0" unist-util-find-all-after "^3.0.2" +"@testing-library/dom@^8.0.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5" + integrity sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + +"@testing-library/react@^12.1.5": + version "12.1.5" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" + integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@types/react-dom" "<18.0.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@types/aria-query@^4.2.0": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" + integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7": version "7.1.19" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" @@ -2107,6 +2135,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@<18.0.0": + version "17.0.17" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.17.tgz#2e3743277a793a96a99f1bf87614598289da68a1" + integrity sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg== + dependencies: + "@types/react" "^17" + "@types/react-redux@^7.1.20": version "7.1.24" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" @@ -2484,6 +2519,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -3744,6 +3784,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.14" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56" + integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg== + dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -6723,6 +6768,11 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -7725,7 +7775,7 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" -pretty-format@^27.5.1: +pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== From abfc66a34e39de4c4ac43964e1fa17b1004ac991 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Jun 2022 12:18:26 +0000 Subject: [PATCH 007/183] Improve _ShareType.scss (#8737) * Specify the button style explicitly removing the dependency on the mixin The reset mixin can cause style inconsistencies by disrupting cascading arbitrarily, increasing the number of specified declarations more than needed. Though it might be useful for development, it is not necessary to use it, makes it difficult to grasp the style structure, and can be removed to optimize the structure. Signed-off-by: Suguru Hirahara * Remove element='button' from AccessibleButton Since AccessibleButton has role='button' by default, setting the element button property is redundant. Signed-off-by: Suguru Hirahara * Protect mx_ShareType_option from being regressed structurally Signed-off-by: Suguru Hirahara * yarn run lint:style --fix Signed-off-by: Suguru Hirahara * Wrap buttons with declarations for spacing box-sizing is not required for the buttons or the wrapper. Signed-off-by: Suguru Hirahara * yarn run lint:style --fix Signed-off-by: Suguru Hirahara * fix eslint errors Signed-off-by: Suguru Hirahara * Fix LocationShareMenu-test.tsx AccessibleButton is div by default Signed-off-by: Suguru Hirahara * Reflect the review Signed-off-by: Suguru Hirahara * Revert "Remove element='button' from AccessibleButton" This reverts commit af78d2713f6b4fca9405498e0090db1e6218ff1b. Signed-off-by: Suguru Hirahara * Revert "Fix LocationShareMenu-test.tsx" This reverts commit 7d783a733ec84af6453b2359b2d00443fcece2ef. Signed-off-by: Suguru Hirahara --- .../components/views/location/_ShareType.scss | 51 +++++++++++-------- src/components/views/location/ShareType.tsx | 20 ++++---- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/res/css/components/views/location/_ShareType.scss b/res/css/components/views/location/_ShareType.scss index 6b39f1a80a4..4dd49d4470d 100644 --- a/res/css/components/views/location/_ShareType.scss +++ b/res/css/components/views/location/_ShareType.scss @@ -24,6 +24,35 @@ limitations under the License. padding: 60px $spacing-12 $spacing-32; color: $primary-content; + + .mx_ShareType_wrapper_options { + display: flex; + flex-direction: column; + row-gap: $spacing-12; + width: 100%; + margin-top: $spacing-12; + + .mx_ShareType_option { + display: flex; + align-items: center; + justify-content: flex-start; + padding: $spacing-8 $spacing-20; + background: none; + + border: 1px solid $quinary-content; + border-radius: 8px; + + font-size: $font-15px; + font-family: inherit; + line-height: inherit; + color: $primary-content; + + &:hover, + &:focus { + border-color: $accent; + } + } + } } .mx_ShareType_badge { @@ -43,28 +72,6 @@ limitations under the License. text-align: center; } -.mx_ShareType_option { - @mixin ButtonResetDefault; - - width: 100%; - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - padding: $spacing-8 $spacing-20; - margin-top: $spacing-12; - - color: $primary-content; - border: 1px solid $quinary-content; - border-radius: 8px; - - font-size: $font-15px; - - &:hover, &:focus { - border-color: $accent; - } -} - .mx_ShareType_option-icon { height: 40px; width: 40px; diff --git a/src/components/views/location/ShareType.tsx b/src/components/views/location/ShareType.tsx index 6f4ad2d2646..61bf018ff26 100644 --- a/src/components/views/location/ShareType.tsx +++ b/src/components/views/location/ShareType.tsx @@ -79,15 +79,17 @@ const ShareType: React.FC = ({ return
{ _t("What location type do you want to share?") } - { enabledShareTypes.map((type) => - setShareType(type)} - label={labels[type]} - shareType={type} - data-test-id={`share-location-option-${type}`} - />, - ) } +
+ { enabledShareTypes.map((type) => + setShareType(type)} + label={labels[type]} + shareType={type} + data-test-id={`share-location-option-${type}`} + />, + ) } +
; }; From f152310c083a6ce5a87c6b81e1b3034acf613947 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 2 Jun 2022 09:10:22 -0400 Subject: [PATCH 008/183] Use random widget IDs for video rooms (#8739) --- src/CallHandler.tsx | 3 +-- src/utils/VideoChannelUtils.ts | 5 ++--- src/utils/WidgetUtils.ts | 8 ++++---- test/components/structures/VideoRoomView-test.tsx | 5 +++-- test/createRoom-test.ts | 5 ++--- test/stores/VideoChannelStore-test.ts | 6 +++--- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 787f602fb77..c6b28d65c44 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -1035,8 +1035,7 @@ export default class CallHandler extends EventEmitter { } try { - const userId = client.credentials.userId; - await WidgetUtils.addJitsiWidget(roomId, type, 'Jitsi', `jitsi_${userId}_${Date.now()}`); + await WidgetUtils.addJitsiWidget(roomId, type, 'Jitsi', false); logger.log('Jitsi widget added'); } catch (e) { if (e.errcode === 'M_FORBIDDEN') { diff --git a/src/utils/VideoChannelUtils.ts b/src/utils/VideoChannelUtils.ts index c4c757e29f1..498ccf63258 100644 --- a/src/utils/VideoChannelUtils.ts +++ b/src/utils/VideoChannelUtils.ts @@ -35,16 +35,15 @@ interface IVideoChannelMemberContent { devices: string[]; } -export const VIDEO_CHANNEL = "io.element.video"; export const VIDEO_CHANNEL_MEMBER = "io.element.video.member"; export const getVideoChannel = (roomId: string): IApp => { const apps = WidgetStore.instance.getApps(roomId); - return apps.find(app => WidgetType.JITSI.matches(app.type) && app.id === VIDEO_CHANNEL); + return apps.find(app => WidgetType.JITSI.matches(app.type) && app.data.isVideoChannel); }; export const addVideoChannel = async (roomId: string, roomName: string) => { - await WidgetUtils.addJitsiWidget(roomId, CallType.Video, "Video channel", VIDEO_CHANNEL, roomName); + await WidgetUtils.addJitsiWidget(roomId, CallType.Video, "Video channel", true, roomName); }; export const getConnectedMembers = (room: Room, connectedLocalEcho: boolean): Set => { diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 8eebed3871c..ee4f97ff45f 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -23,7 +23,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring"; +import { randomString, randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring"; import { MatrixClientPeg } from '../MatrixClientPeg'; import SdkConfig from "../SdkConfig"; @@ -35,7 +35,6 @@ import { Jitsi } from "../widgets/Jitsi"; import { objectClone } from "./objects"; import { _t } from "../languageHandler"; import { IApp } from "../stores/WidgetStore"; -import { VIDEO_CHANNEL } from "./VideoChannelUtils"; // How long we wait for the state event echo to come back from the server // before waitFor[Room/User]Widget rejects its promise @@ -444,11 +443,12 @@ export default class WidgetUtils { roomId: string, type: CallType, name: string, - widgetId: string, + isVideoChannel: boolean, oobRoomName?: string, ): Promise { const domain = Jitsi.getInstance().preferredDomain; const auth = await Jitsi.getInstance().getJitsiAuth(); + const widgetId = randomString(24); // Must be globally unique let confId; if (auth === 'openidtoken-jwt') { @@ -471,7 +471,7 @@ export default class WidgetUtils { conferenceId: confId, roomName: oobRoomName ?? MatrixClientPeg.get().getRoom(roomId)?.name, isAudioOnly: type === CallType.Voice, - isVideoChannel: widgetId === VIDEO_CHANNEL, + isVideoChannel, domain, auth, }); diff --git a/test/components/structures/VideoRoomView-test.tsx b/test/components/structures/VideoRoomView-test.tsx index cf73c8a1394..02d82bb27ca 100644 --- a/test/components/structures/VideoRoomView-test.tsx +++ b/test/components/structures/VideoRoomView-test.tsx @@ -32,7 +32,7 @@ import { mkVideoChannelMember, } from "../../test-utils"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; -import { VIDEO_CHANNEL, VIDEO_CHANNEL_MEMBER } from "../../../src/utils/VideoChannelUtils"; +import { VIDEO_CHANNEL_MEMBER } from "../../../src/utils/VideoChannelUtils"; import WidgetStore from "../../../src/stores/WidgetStore"; import _VideoRoomView from "../../../src/components/structures/VideoRoomView"; import VideoLobby from "../../../src/components/views/voip/VideoLobby"; @@ -42,7 +42,7 @@ const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView); describe("VideoRoomView", () => { jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{ - id: VIDEO_CHANNEL, + id: "1", eventId: "$1:example.org", roomId: "!1:example.org", type: MatrixWidgetType.JitsiMeet, @@ -50,6 +50,7 @@ describe("VideoRoomView", () => { name: "Video channel", creatorUserId: "@alice:example.org", avatar_url: null, + data: { isVideoChannel: true }, }]); Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] }, diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts index c37edaff86a..b10de07e6a2 100644 --- a/test/createRoom-test.ts +++ b/test/createRoom-test.ts @@ -23,7 +23,7 @@ import { stubClient, setupAsyncStoreWithClient } from "./test-utils"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; import WidgetStore from "../src/stores/WidgetStore"; import WidgetUtils from "../src/utils/WidgetUtils"; -import { VIDEO_CHANNEL, VIDEO_CHANNEL_MEMBER } from "../src/utils/VideoChannelUtils"; +import { VIDEO_CHANNEL_MEMBER } from "../src/utils/VideoChannelUtils"; import createRoom, { canEncryptToAllUsers } from '../src/createRoom'; describe("createRoom", () => { @@ -43,12 +43,11 @@ describe("createRoom", () => { events: { [VIDEO_CHANNEL_MEMBER]: videoMemberPower }, }, }]] = mocked(client.createRoom).mock.calls as any; // no good type - const [[widgetRoomId, widgetStateKey, , widgetId]] = mocked(client.sendStateEvent).mock.calls; + const [[widgetRoomId, widgetStateKey]] = mocked(client.sendStateEvent).mock.calls; // We should have set up the Jitsi widget expect(widgetRoomId).toEqual(roomId); expect(widgetStateKey).toEqual("im.vector.modular.widgets"); - expect(widgetId).toEqual(VIDEO_CHANNEL); // All members should be able to update their connected devices expect(videoMemberPower).toEqual(0); diff --git a/test/stores/VideoChannelStore-test.ts b/test/stores/VideoChannelStore-test.ts index cf5def33347..acc44689b5c 100644 --- a/test/stores/VideoChannelStore-test.ts +++ b/test/stores/VideoChannelStore-test.ts @@ -23,14 +23,13 @@ import WidgetStore, { IApp } from "../../src/stores/WidgetStore"; import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingStore"; import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions"; import VideoChannelStore, { VideoChannelEvent } from "../../src/stores/VideoChannelStore"; -import { VIDEO_CHANNEL } from "../../src/utils/VideoChannelUtils"; describe("VideoChannelStore", () => { const store = VideoChannelStore.instance; - const widget = { id: VIDEO_CHANNEL } as unknown as Widget; + const widget = { id: "1" } as unknown as Widget; const app = { - id: VIDEO_CHANNEL, + id: "1", eventId: "$1:example.org", roomId: "!1:example.org", type: MatrixWidgetType.JitsiMeet, @@ -38,6 +37,7 @@ describe("VideoChannelStore", () => { name: "Video channel", creatorUserId: "@alice:example.org", avatar_url: null, + data: { isVideoChannel: true }, } as IApp; // Set up mocks to simulate the remote end of the widget API From 4b957b57af65bd16eb445b8720c3b5aa577c4388 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 2 Jun 2022 09:10:41 -0400 Subject: [PATCH 009/183] Squish event bubble tiles less (#8740) --- res/css/views/messages/_EventTileBubble.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_EventTileBubble.scss b/res/css/views/messages/_EventTileBubble.scss index ff88015e9cf..3f2475663fd 100644 --- a/res/css/views/messages/_EventTileBubble.scss +++ b/res/css/views/messages/_EventTileBubble.scss @@ -19,7 +19,7 @@ limitations under the License. padding: 10px; border-radius: 8px; margin: 10px auto; - max-width: 75%; + max-width: min(90%, 600px); box-sizing: border-box; display: grid; grid-template-columns: 24px minmax(0, 1fr) min-content min-content; From a74b9a70836b97b5a05a18c8e7ed2d7dcd9d8b7e Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 2 Jun 2022 15:59:40 +0200 Subject: [PATCH 010/183] Prevent Invite and DevTools dialogs from being cut off (#8646) * fix: replace fixed height based styling with flex for invite and dev dialogs * feat: create flex wrapper class for dialogs * feat: make invite dialogs use flex layout * feat: make devtools dialogs use flex layout --- res/css/views/dialogs/_DevtoolsDialog.scss | 14 +++-- res/css/views/dialogs/_InviteDialog.scss | 59 +++++++++++++--------- src/RoomInvite.tsx | 4 +- src/utils/DialogOpener.ts | 9 ++-- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index b1fed0d29bd..967d7c2fb9b 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -16,18 +16,24 @@ limitations under the License. .mx_DevtoolsDialog_wrapper { .mx_Dialog { - height: 100%; + display: flex; + flex-direction: column; } .mx_Dialog_fixedWidth { - overflow-y: hidden; - height: 100%; + display: flex; + flex-direction: column; + min-height: 0; + max-height: 100%; + + .mx_Dialog_buttons button { + margin-bottom: 0; + } } } .mx_DevTools_content { overflow-y: auto; - height: calc(100% - 124px); // 58px for buttons + 50px for header + 8px margin around } .mx_DevTools_RoomStateExplorer_query { diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 35fedd7cf28..6420cf61790 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -14,6 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_InviteDialog_flexWrapper .mx_Dialog { + display: flex; + flex-direction: column; +} + .mx_InviteDialog_transferWrapper .mx_Dialog { padding-bottom: 16px; } @@ -128,8 +133,9 @@ limitations under the License. text-transform: uppercase; } - .mx_CopyableText { + .mx_CopyableText.mx_CopyableText_border { width: unset; // full width + margin-bottom: 0; > a { text-decoration: none; @@ -270,14 +276,16 @@ limitations under the License. .mx_InviteDialog_other { // Prevent the dialog from jumping around randomly when elements change. - height: 600px; + display: flex; + flex-direction: column; + max-height: 600px; + overflow: hidden; .mx_InviteDialog_addressBar { margin-right: 0; } .mx_InviteDialog_userSections { - height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements padding-right: 0; .mx_InviteDialog_section { @@ -285,29 +293,37 @@ limitations under the License. margin-top: 12px; } } - - .mx_InviteDialog_hasFooter { - .mx_InviteDialog_userSections { - height: calc(100% - 175px); // For displaying an invite link on the footer of the dialog - } - } } .mx_InviteDialog_content { - height: calc(100% - 36px); // full height minus the size of the header + display: flex; + flex-direction: column; + flex-shrink: 1; overflow: hidden; } .mx_InviteDialog_transfer { - width: 496px; - height: 466px; - flex-direction: column; + width: auto; .mx_InviteDialog_content { - flex-direction: column; + width: 496px; + height: 430px; .mx_TabbedView { - height: calc(100% - 60px); + display: flex; + flex-direction: column; + flex-shrink: 1; + flex-grow: 1; + min-height: 0; + + .mx_TabbedView_tabPanel { + flex-direction: column; + + .mx_TabbedView_tabPanelContent { + display: flex; + flex-direction: column; + } + } } overflow: visible; } @@ -327,10 +343,6 @@ limitations under the License. padding: 0 45px 4px 0; } -.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections { - height: calc(100% - 175px); -} - .mx_InviteDialog_helpText { margin: 0; } @@ -380,14 +392,13 @@ limitations under the License. .mx_InviteDialog_transferConsultConnect { padding-top: 16px; - /* This wants a drop shadow the full width of the dialog, so relative-position it - * and make it wider, then compensate with padding + /* This wants a drop shadow the full width of the dialog, so use negative margin to make it full width, + * then compensate with padding */ - position: relative; - width: 496px; - left: -24px; padding-left: 24px; padding-right: 24px; + margin-left: -24px; + margin-right: -24px; border-top: 1px solid $quinary-content; display: flex; diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index e9204996ed2..f7cad382543 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -63,7 +63,7 @@ export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. Modal.createTrackedDialog( 'Start DM', '', InviteDialog, { kind: KIND_DM, initialText }, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + /*className=*/"mx_InviteDialog_flexWrapper", /*isPriority=*/false, /*isStatic=*/true, ); } @@ -75,7 +75,7 @@ export function showRoomInviteDialog(roomId: string, initialText = ""): void { initialText, roomId, }, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + /*className=*/"mx_InviteDialog_flexWrapper", /*isPriority=*/false, /*isStatic=*/true, ); } diff --git a/src/utils/DialogOpener.ts b/src/utils/DialogOpener.ts index 8c5c9c374ba..8bd6c137342 100644 --- a/src/utils/DialogOpener.ts +++ b/src/utils/DialogOpener.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import classnames from "classnames"; + import defaultDispatcher from "../dispatcher/dispatcher"; import { ActionPayload } from "../dispatcher/payloads"; import Modal from "../Modal"; @@ -89,9 +91,10 @@ export class DialogOpener { kind: payload.kind, call: payload.call, roomId: payload.roomId, - }, payload.className, false, true).finished.then((results) => { - payload.onFinishedCallback?.(results); - }); + }, classnames("mx_InviteDialog_flexWrapper", payload.className), false, true).finished + .then((results) => { + payload.onFinishedCallback?.(results); + }); break; case Action.OpenAddToExistingSpaceDialog: { const space = payload.space; From 79a2dfe171ec509239a135850ceae6ee580261f0 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 2 Jun 2022 17:43:19 +0200 Subject: [PATCH 011/183] Live location share - enable reply and react to tiles (#8721) * test most basic paths in messageactionbar Signed-off-by: Kerry Archibald * tidy Signed-off-by: Kerry Archibald * use rtl for MessageActionBar test Signed-off-by: Kerry Archibald * make beacon_info events semi actionable Signed-off-by: Kerry Archibald * remove log Signed-off-by: Kerry Archibald * test thread exception for beacon Signed-off-by: Kerry Archibald * eat click events in beacon status to stop jumping from reply tile Signed-off-by: Kerry Archibald * set max width on beaconbody for render in thread panel --- .../views/messages/_MBeaconBody.scss | 3 +- .../views/beacon/OwnBeaconStatus.tsx | 16 ++- .../views/messages/MessageActionBar.tsx | 11 +- src/utils/EventUtils.ts | 8 +- .../views/messages/MessageActionBar-test.tsx | 104 ++++++++++++++++++ test/utils/EventUtils-test.ts | 11 +- 6 files changed, 143 insertions(+), 10 deletions(-) diff --git a/res/css/components/views/messages/_MBeaconBody.scss b/res/css/components/views/messages/_MBeaconBody.scss index 5654f14a057..64d7908df06 100644 --- a/res/css/components/views/messages/_MBeaconBody.scss +++ b/res/css/components/views/messages/_MBeaconBody.scss @@ -17,7 +17,8 @@ limitations under the License. .mx_MBeaconBody { position: relative; height: 220px; - width: 325px; + max-width: 325px; + width: 100%; border-radius: $timeline-image-border-radius; overflow: hidden; diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx index 3ef2de7a72a..87603761317 100644 --- a/src/components/views/beacon/OwnBeaconStatus.tsx +++ b/src/components/views/beacon/OwnBeaconStatus.tsx @@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler'; import { useOwnLiveBeacons } from '../../../utils/beacon'; import BeaconStatus from './BeaconStatus'; import { BeaconDisplayStatus } from './displayStatus'; -import AccessibleButton from '../elements/AccessibleButton'; +import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton'; interface Props { displayStatus: BeaconDisplayStatus; @@ -45,6 +45,14 @@ const OwnBeaconStatus: React.FC> = ({ onResetLocationPublishError, } = useOwnLiveBeacons([beacon?.identifier]); + // eat events here to avoid 1) the map and 2) reply or thread tiles + // moving under the beacon status on stop/retry click + const preventDefaultWrapper = (callback: () => void) => (e?: ButtonEvent) => { + e?.stopPropagation(); + e?.preventDefault(); + callback(); + }; + // combine display status with errors that only occur for user's own beacons const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ? BeaconDisplayStatus.Error : @@ -60,7 +68,7 @@ const OwnBeaconStatus: React.FC> = ({ { ownDisplayStatus === BeaconDisplayStatus.Active && @@ -70,7 +78,7 @@ const OwnBeaconStatus: React.FC> = ({ { hasLocationPublishError && { _t('Retry') } @@ -79,7 +87,7 @@ const OwnBeaconStatus: React.FC> = ({ { hasStopSharingError && { _t('Retry') } diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 7bd7fd719e8..690d4d60bb9 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -21,6 +21,7 @@ import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/mo import classNames from 'classnames'; import { MsgType, RelationType } from 'matrix-js-sdk/src/@types/event'; import { Thread } from 'matrix-js-sdk/src/models/thread'; +import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon'; import type { Relations } from 'matrix-js-sdk/src/models/relations'; import { _t } from '../../../languageHandler'; @@ -329,8 +330,14 @@ export default class MessageActionBar extends React.PureComponent { export function canForward(event: MatrixEvent): boolean { return !( - M_POLL_START.matches(event.getType()) + M_POLL_START.matches(event.getType()) || + // disallow forwarding until psf-1044 + M_BEACON_INFO.matches(event.getType()) ); } diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx index 3d624187b8c..97b5c0c0770 100644 --- a/test/components/views/messages/MessageActionBar-test.tsx +++ b/test/components/views/messages/MessageActionBar-test.tsx @@ -25,20 +25,28 @@ import { MsgType, Room, } from 'matrix-js-sdk/src/matrix'; +import { Thread } from 'matrix-js-sdk/src/models/thread'; import MessageActionBar from '../../../../src/components/views/messages/MessageActionBar'; import { getMockClientWithEventEmitter, mockClientMethodsUser, mockClientMethodsEvents, + makeBeaconInfoEvent, } from '../../../test-utils'; import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; import RoomContext, { TimelineRenderingType } from '../../../../src/contexts/RoomContext'; import { IRoomState } from '../../../../src/components/structures/RoomView'; import dispatcher from '../../../../src/dispatcher/dispatcher'; import SettingsStore from '../../../../src/settings/SettingsStore'; +import { Action } from '../../../../src/dispatcher/actions'; +import { UserTab } from '../../../../src/components/views/dialogs/UserTab'; +import { showThread } from '../../../../src/dispatcher/dispatch-actions/threads'; jest.mock('../../../../src/dispatcher/dispatcher'); +jest.mock('../../../../src/dispatcher/dispatch-actions/threads', () => ({ + showThread: jest.fn(), +})); describe('', () => { const userId = '@alice:server.org'; @@ -360,4 +368,100 @@ describe('', () => { it.todo('unsends event on cancel click'); it.todo('retrys event on retry click'); }); + + describe('thread button', () => { + beforeEach(() => { + Thread.setServerSideSupport(true, false); + }); + + describe('when threads feature is not enabled', () => { + it('does not render thread button when threads does not have server support', () => { + jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + Thread.setServerSideSupport(false, false); + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('Reply in thread')).toBeFalsy(); + }); + + it('renders thread button when threads has server support', () => { + jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('Reply in thread')).toBeTruthy(); + }); + + it('opens user settings on click', () => { + jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + + act(() => { + fireEvent.click(getByLabelText('Reply in thread')); + }); + + expect(dispatcher.dispatch).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Labs, + }); + }); + }); + + describe('when threads feature is enabled', () => { + beforeEach(() => { + jest.spyOn(SettingsStore, 'getValue').mockImplementation(setting => setting === 'feature_thread'); + }); + + it('renders thread button on own actionable event', () => { + const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + expect(queryByLabelText('Reply in thread')).toBeTruthy(); + }); + + it('does not render thread button for a beacon_info event', () => { + const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId); + const { queryByLabelText } = getComponent({ mxEvent: beaconInfoEvent }); + expect(queryByLabelText('Reply in thread')).toBeFalsy(); + }); + + it('opens thread on click', () => { + const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + + act(() => { + fireEvent.click(getByLabelText('Reply in thread')); + }); + + expect(showThread).toHaveBeenCalledWith({ + rootEvent: alicesMessageEvent, + push: false, + }); + }); + + it('opens parent thread for a thread reply message', () => { + const threadReplyEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'this is a thread reply', + }, + }); + // mock the thread stuff + jest.spyOn(threadReplyEvent, 'isThreadRelation', 'get').mockReturnValue(true); + // set alicesMessageEvent as the root event + jest.spyOn(threadReplyEvent, 'getThread').mockReturnValue( + { rootEvent: alicesMessageEvent } as unknown as Thread, + ); + const { getByLabelText } = getComponent({ mxEvent: threadReplyEvent }); + + act(() => { + fireEvent.click(getByLabelText('Reply in thread')); + }); + + expect(showThread).toHaveBeenCalledWith({ + rootEvent: alicesMessageEvent, + initialEvent: threadReplyEvent, + highlighted: true, + scroll_into_view: true, + push: false, + }); + }); + }); + }); }); diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts index df65b7e5f35..49bc26b4efc 100644 --- a/test/utils/EventUtils-test.ts +++ b/test/utils/EventUtils-test.ts @@ -62,7 +62,11 @@ describe('EventUtils', () => { }); redactedEvent.makeRedacted(redactedEvent); - const stateEvent = makeBeaconInfoEvent(userId, roomId); + const stateEvent = new MatrixEvent({ + type: EventType.RoomTopic, + state_key: '', + }); + const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId); const roomMemberEvent = new MatrixEvent({ type: EventType.RoomMember, @@ -155,6 +159,7 @@ describe('EventUtils', () => { ['poll start event', pollStartEvent], ['event with empty content body', emptyContentBody], ['event with a content body', niceTextMessage], + ['beacon_info event', beaconInfoEvent], ])('returns true for %s', (_description, event) => { expect(isContentActionable(event)).toBe(true); }); @@ -325,6 +330,10 @@ describe('EventUtils', () => { const event = makePollStartEvent('Who?', userId); expect(canForward(event)).toBe(false); }); + it('returns false for a beacon_info event', () => { + const event = makeBeaconInfoEvent(userId, roomId); + expect(canForward(event)).toBe(false); + }); it('returns true for a room message event', () => { const event = new MatrixEvent({ type: EventType.RoomMessage, From 68bc8112b3a7607b775bddc97776ceda4f51e480 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Jun 2022 17:26:48 +0000 Subject: [PATCH 012/183] Adjust message timestamp position on TimelineCard in non-bubble layouts (#8745) --- res/css/views/right_panel/_TimelineCard.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index e2711b8c594..f3e6ac393c1 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -87,8 +87,8 @@ limitations under the License. .mx_MessageTimestamp { position: absolute; // for modern layout and IRC layout - right: -4px; - left: auto; + inset-inline-start: auto; + inset-inline-end: 0; } .mx_EventTile_msgOption { From a85799b87c374fe8dd61ed8dcb4c6e02d8a32a26 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 2 Jun 2022 14:11:28 -0400 Subject: [PATCH 013/183] Make PiP motion smoother and react to window resizes correctly (#8747) * Make PiP motion smoother and react to window resizes correctly * Remove debugging logs * Apply code review suggestions --- src/components/views/elements/AppTile.tsx | 10 +- .../views/elements/PersistedElement.tsx | 12 ++- .../views/elements/PersistentApp.tsx | 6 +- .../views/voip/PictureInPictureDragger.tsx | 101 ++++++++---------- src/components/views/voip/PipView.tsx | 7 +- 5 files changed, 71 insertions(+), 65 deletions(-) diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index f202a1e5705..bdb591fe19a 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -18,7 +18,7 @@ limitations under the License. */ import url from 'url'; -import React, { ContextType, createRef } from 'react'; +import React, { ContextType, createRef, MutableRefObject } from 'react'; import classNames from 'classnames'; import { MatrixCapabilities } from "matrix-widget-api"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; @@ -84,6 +84,8 @@ interface IProps { pointerEvents?: string; widgetPageTitle?: string; showLayoutButtons?: boolean; + // Handle to manually notify the PersistedElement that it needs to move + movePersistedElement?: MutableRefObject<() => void>; } interface IState { @@ -623,7 +625,11 @@ export default class AppTile extends React.Component { const zIndexAboveOtherPersistentElements = 101; appTileBody =
- + { appTileBody }
; diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index cd8239a1f19..5d61bd5d81e 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { MutableRefObject } from 'react'; import ReactDOM from 'react-dom'; import { throttle } from "lodash"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; @@ -56,6 +56,9 @@ interface IProps { zIndex?: number; style?: React.StyleHTMLAttributes; + + // Handle to manually notify this PersistedElement that it needs to move + moveRef?: MutableRefObject<() => void>; } /** @@ -86,6 +89,8 @@ export default class PersistedElement extends React.Component { // the timeline_resize action. window.addEventListener('resize', this.repositionChild); this.dispatcherRef = dis.register(this.onAction); + + if (this.props.moveRef) this.props.moveRef.current = this.repositionChild; } /** @@ -177,8 +182,9 @@ export default class PersistedElement extends React.Component { Object.assign(child.style, { zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex, position: 'absolute', - top: parentRect.top + 'px', - left: parentRect.left + 'px', + top: '0', + left: '0', + transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`, width: parentRect.width + 'px', height: parentRect.height + 'px', }); diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 5851c1c614d..f0ad74f09e4 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -1,6 +1,6 @@ /* Copyright 2018 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019-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. @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ContextType } from 'react'; +import React, { ContextType, MutableRefObject } from 'react'; import { Room } from "matrix-js-sdk/src/models/room"; import WidgetUtils from '../../../utils/WidgetUtils'; @@ -27,6 +27,7 @@ interface IProps { persistentWidgetId: string; persistentRoomId: string; pointerEvents?: string; + movePersistedElement: MutableRefObject<() => void>; } export default class PersistentApp extends React.Component { @@ -70,6 +71,7 @@ export default class PersistentApp extends React.Component { miniMode={true} showMenubar={false} pointerEvents={this.props.pointerEvents} + movePersistedElement={this.props.movePersistedElement} />; } return null; diff --git a/src/components/views/voip/PictureInPictureDragger.tsx b/src/components/views/voip/PictureInPictureDragger.tsx index be32ab2cd9a..4a2ac739530 100644 --- a/src/components/views/voip/PictureInPictureDragger.tsx +++ b/src/components/views/voip/PictureInPictureDragger.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 New Vector Ltd +Copyright 2021-2022 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ limitations under the License. import React, { createRef } from 'react'; -import UIStore from '../../../stores/UIStore'; +import UIStore, { UI_EVENTS } from '../../../stores/UIStore'; import { lerp } from '../../../utils/AnimationUtils'; import { MarkedExecution } from '../../../utils/MarkedExecution'; @@ -43,69 +43,66 @@ interface IProps { children: ({ onStartMoving, onResize }: IChildrenOptions) => React.ReactNode; draggable: boolean; onDoubleClick?: () => void; -} - -interface IState { - // Position of the PictureInPictureDragger - translationX: number; - translationY: number; + onMove?: () => void; } /** * PictureInPictureDragger shows a small version of CallView hovering over the UI in 'picture-in-picture' * (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing. */ -export default class PictureInPictureDragger extends React.Component { +export default class PictureInPictureDragger extends React.Component { private callViewWrapper = createRef(); private initX = 0; private initY = 0; private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH; private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT; + private translationX = this.desiredTranslationX; + private translationY = this.desiredTranslationY; private moving = false; private scheduledUpdate = new MarkedExecution( () => this.animationCallback(), () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), ); - constructor(props: IProps) { - super(props); - - this.state = { - translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH, - translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT, - }; - } - public componentDidMount() { document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); - window.addEventListener("resize", this.onResize); + UIStore.instance.on(UI_EVENTS.Resize, this.onResize); } public componentWillUnmount() { document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); - window.removeEventListener("resize", this.onResize); + UIStore.instance.off(UI_EVENTS.Resize, this.onResize); } private animationCallback = () => { - // If the PiP isn't being dragged and there is only a tiny difference in - // the desiredTranslation and translation, quit the animationCallback - // loop. If that is the case, it means the PiP has snapped into its - // position and there is nothing to do. Not doing this would cause an - // infinite loop if ( !this.moving && - Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 && - Math.abs(this.state.translationY - this.desiredTranslationY) <= 1 - ) return; - - const amt = this.moving ? MOVING_AMT : SNAPPING_AMT; - this.setState({ - translationX: lerp(this.state.translationX, this.desiredTranslationX, amt), - translationY: lerp(this.state.translationY, this.desiredTranslationY, amt), - }); - this.scheduledUpdate.mark(); + Math.abs(this.translationX - this.desiredTranslationX) <= 1 && + Math.abs(this.translationY - this.desiredTranslationY) <= 1 + ) { + // Break the loop by settling the element into its final position + this.translationX = this.desiredTranslationX; + this.translationY = this.desiredTranslationY; + this.setStyle(); + } else { + const amt = this.moving ? MOVING_AMT : SNAPPING_AMT; + this.translationX = lerp(this.translationX, this.desiredTranslationX, amt); + this.translationY = lerp(this.translationY, this.desiredTranslationY, amt); + + this.setStyle(); + this.scheduledUpdate.mark(); + } + + this.props.onMove?.(); + }; + + private setStyle = () => { + if (!this.callViewWrapper.current) return; + // Set the element's style directly, bypassing React for efficiency + this.callViewWrapper.current.style.transform = + `translateX(${this.translationX}px) translateY(${this.translationY}px)`; }; private setTranslation(inTranslationX: number, inTranslationY: number) { @@ -164,20 +161,14 @@ export default class PictureInPictureDragger extends React.Component { @@ -205,25 +196,21 @@ export default class PictureInPictureDragger extends React.Component - <> - { this.props.children({ - onStartMoving: this.onStartMoving, - onResize: this.onResize, - }) } - + { this.props.children({ + onStartMoving: this.onStartMoving, + onResize: this.onResize, + }) } ); } diff --git a/src/components/views/voip/PipView.tsx b/src/components/views/voip/PipView.tsx index 613a542d70f..42462de7dd9 100644 --- a/src/components/views/voip/PipView.tsx +++ b/src/components/views/voip/PipView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { createRef } from 'react'; import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { EventSubscription } from 'fbemitter'; import { logger } from "matrix-js-sdk/src/logger"; @@ -118,6 +118,7 @@ function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall export default class PipView extends React.Component { private roomStoreToken: EventSubscription; private settingsWatcherRef: string; + private movePersistedElement = createRef<() => void>(); constructor(props: IProps) { super(props); @@ -176,6 +177,8 @@ export default class PipView extends React.Component { this.setState({ moving: false }); } + private onMove = () => this.movePersistedElement.current?.(); + private onRoomViewStoreUpdate = () => { const newRoomId = RoomViewStore.instance.getRoomId(); const oldRoomId = this.state.viewedRoomId; @@ -338,6 +341,7 @@ export default class PipView extends React.Component { persistentWidgetId={this.state.persistentWidgetId} persistentRoomId={roomId} pointerEvents={this.state.moving ? 'none' : undefined} + movePersistedElement={this.movePersistedElement} /> ; } @@ -347,6 +351,7 @@ export default class PipView extends React.Component { className="mx_CallPreview" draggable={pipMode} onDoubleClick={this.onDoubleClick} + onMove={this.onMove} > { pipContent } ; From 54239464fa208c9d861a4cac3dd16c35aea6c35a Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 3 Jun 2022 02:38:39 +0000 Subject: [PATCH 014/183] Make sure MessageTimestamp is not hidden by EventTile_line (#8748) Signed-off-by: Suguru Hirahara --- res/css/views/right_panel/_TimelineCard.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index f3e6ac393c1..8b3064e5295 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -55,6 +55,7 @@ limitations under the License. &.mx_EventTile_info .mx_EventTile_line, .mx_EventTile_line { padding: var(--BaseCard_EventTile_line-padding-block) var(--BaseCard_EventTile-spacing-horizontal); + padding-inline-end: $MessageTimestamp_width; // ensure timestamp is not hidden .mx_EventTile_e2eIcon { inset-inline-start: 8px; From b5ed051ecc8b2e0ae7d417df4759468fa87fd96d Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 3 Jun 2022 06:07:18 +0000 Subject: [PATCH 015/183] Use AccessibleButton for 'Reset All' link button on SetupEncryptionBody (#8730) - Remove ButtonResetDefault to respect the concept of cascading Signed-off-by: Suguru Hirahara --- res/css/structures/auth/_SetupEncryptionBody.scss | 10 ++++++---- src/components/structures/auth/SetupEncryptionBody.tsx | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/res/css/structures/auth/_SetupEncryptionBody.scss b/res/css/structures/auth/_SetupEncryptionBody.scss index 1999fb581db..52651ec2a98 100644 --- a/res/css/structures/auth/_SetupEncryptionBody.scss +++ b/res/css/structures/auth/_SetupEncryptionBody.scss @@ -17,9 +17,11 @@ limitations under the License. .mx_SetupEncryptionBody_reset { color: $light-fg-color; margin-top: $font-14px; -} -.mx_SetupEncryptionBody_reset_link { - @mixin ButtonResetDefault; - color: $alert; + .mx_SetupEncryptionBody_reset_link { + &.mx_AccessibleButton_kind_link_inline { + padding: 0; + color: $alert; + } + } } diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 2553f8fbaa0..5caaea0c988 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -212,11 +212,13 @@ export default class SetupEncryptionBody extends React.Component
{ _t("Forgotten or lost all recovery methods? Reset all", null, { - a: (sub) => , + , }) }
From 228abb6f076c36ff6b97247c04fe0a4363ad67dc Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 3 Jun 2022 06:45:10 +0000 Subject: [PATCH 016/183] Move style rules of MatrixChat_useCompactLayout from _GroupLayout.scss to _EventTile.scss and _RoomView.scss (#8725) * Move mx_RoomView_MessageList h2 block from _GroupLayout.scss to _RoomView.scss Signed-off-by: Suguru Hirahara * Move mx_MatrixChat_useCompactLayout block from _GroupLayout.scss to _EventTile.scss This block is not related to the group layout. Signed-off-by: Suguru Hirahara * Include EventTile_continuation block in EventTile_emote Signed-off-by: Suguru Hirahara * Use logical properties for the padding values Signed-off-by: Suguru Hirahara * Sort declarations for maintainability - EventTile_avatar - EventTile_line - EventTile_reply Signed-off-by: Suguru Hirahara * yarn run lint:style --fix Signed-off-by: Suguru Hirahara * Separate selectors of .markdown-body Signed-off-by: Suguru Hirahara --- res/css/structures/_RoomView.scss | 4 ++ res/css/views/rooms/_EventTile.scss | 80 ++++++++++++++++++++++++++ res/css/views/rooms/_GroupLayout.scss | 82 --------------------------- 3 files changed, 84 insertions(+), 82 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index eba8ae8f6e8..b19173a3139 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -272,6 +272,10 @@ hr.mx_RoomView_myReadMarker { .mx_MatrixChat_useCompactLayout { .mx_RoomView_MessageList { margin-bottom: 4px; + + h2 { + margin-top: 6px; // TODO: Use a spacing variable + } } .mx_RoomView_statusAreaBox { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3f2a1fbf19c..13eb3c27dfb 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -610,6 +610,86 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss } } +/* Compact layout overrides */ + +.mx_MatrixChat_useCompactLayout { + .mx_EventTile { + padding-top: $spacing-4; + + &.mx_EventTile_info { + padding-top: 0; // same as the padding for non-compact .mx_EventTile.mx_EventTile_info + font-size: $font-13px; + + .mx_EventTile_avatar { + top: 4px; + } + + .mx_EventTile_line, + .mx_EventTile_reply { + line-height: $font-20px; + } + } + + &.mx_EventTile_emote { + padding-top: $spacing-8; // add a bit more space for emotes so that avatars don't collide + + &.mx_EventTile_continuation { + padding-top: 0; + + .mx_EventTile_line, + .mx_EventTile_reply { + padding-block: 0; + } + } + + .mx_EventTile_avatar { + top: 2px; + } + + .mx_EventTile_line, + .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 1px; + } + } + + .mx_EventTile_avatar { + top: 2px; + } + + .mx_EventTile_line, + .mx_EventTile_reply { + padding-block: 0; + } + + .mx_EventTile_e2eIcon { + top: 3px; + } + + .mx_DisambiguatedProfile { + font-size: $font-13px; + } + + .mx_ReadReceiptGroup { + // This aligns the avatar with the last line of the + // message. We want to move it one line up - 2rem + top: -2rem; + } + + .mx_EventTile_content .markdown-body { + p, + ul, + ol, + dl, + blockquote, + pre, + table { + margin-bottom: $spacing-4; // 1/4 of the non-compact margin-bottom + } + } + } +} + /* end of overrides */ .mx_EventTile_keyRequestInfo { diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index 86c0eb176e1..aab87812540 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -46,85 +46,3 @@ $left-gutter: 64px; } } } - -/* Compact layout overrides */ - -.mx_MatrixChat_useCompactLayout { - .mx_EventTile { - padding-top: 4px; - - .mx_EventTile_line, - .mx_EventTile_reply { - padding-top: 0; - padding-bottom: 0; - } - - &.mx_EventTile_info { - // same as the padding for non-compact .mx_EventTile.mx_EventTile_info - padding-top: 0px; - font-size: $font-13px; - - .mx_EventTile_line, - .mx_EventTile_reply { - line-height: $font-20px; - } - - .mx_EventTile_avatar { - top: 4px; - } - } - - .mx_DisambiguatedProfile { - font-size: $font-13px; - } - - &.mx_EventTile_emote { - // add a bit more space for emotes so that avatars don't collide - padding-top: 8px; - - .mx_EventTile_avatar { - top: 2px; - } - - .mx_EventTile_line, - .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 1px; - } - } - - &.mx_EventTile_emote.mx_EventTile_continuation { - padding-top: 0; - - .mx_EventTile_line, - .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } - } - - .mx_EventTile_avatar { - top: 2px; - } - - .mx_EventTile_e2eIcon { - top: 3px; - } - - .mx_ReadReceiptGroup { - // This aligns the avatar with the last line of the - // message. We want to move it one line up - 2rem - top: -2rem; - } - - .mx_EventTile_content .markdown-body { - p, ul, ol, dl, blockquote, pre, table { - margin-bottom: 4px; // 1/4 of the non-compact margin-bottom - } - } - } - - .mx_RoomView_MessageList h2 { - margin-top: 6px; - } -} From 91cbd4dc8ad8e9c51d460438ea469fefde02b0ab Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 3 Jun 2022 10:28:19 +0200 Subject: [PATCH 017/183] hide live location option when composer has relation (#8746) Signed-off-by: Kerry Archibald --- .../views/location/LocationShareMenu.tsx | 14 +++++++++++--- .../views/location/LocationShareMenu-test.tsx | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/components/views/location/LocationShareMenu.tsx b/src/components/views/location/LocationShareMenu.tsx index 795a7802375..7b31b45b04f 100644 --- a/src/components/views/location/LocationShareMenu.tsx +++ b/src/components/views/location/LocationShareMenu.tsx @@ -38,8 +38,16 @@ type Props = Omit & { relation?: IEventRelation; }; -const getEnabledShareTypes = (): LocationShareType[] => { - const enabledShareTypes = [LocationShareType.Own, LocationShareType.Live]; +const getEnabledShareTypes = (relation): LocationShareType[] => { + const enabledShareTypes = [ + LocationShareType.Own, + ]; + + // live locations cannot have a relation + // hide the option when composer has a relation + if (!relation) { + enabledShareTypes.push(LocationShareType.Live); + } if (SettingsStore.getValue("feature_location_share_pin_drop")) { enabledShareTypes.push(LocationShareType.Pin); @@ -57,7 +65,7 @@ const LocationShareMenu: React.FC = ({ relation, }) => { const matrixClient = useContext(MatrixClientContext); - const enabledShareTypes = getEnabledShareTypes(); + const enabledShareTypes = getEnabledShareTypes(relation); const isLiveShareEnabled = useFeatureEnabled("feature_location_share_live"); const multipleShareTypesEnabled = enabledShareTypes.length > 1; diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index eca076d705d..3445d49450e 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -16,12 +16,13 @@ limitations under the License. import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; +import { mocked } from 'jest-mock'; import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { mocked } from 'jest-mock'; -import { act } from 'react-dom/test-utils'; -import { M_ASSET, LocationAssetType } from 'matrix-js-sdk/src/@types/location'; +import { RelationType } from 'matrix-js-sdk/src/matrix'; import { logger } from 'matrix-js-sdk/src/logger'; +import { M_ASSET, LocationAssetType } from 'matrix-js-sdk/src/@types/location'; +import { act } from 'react-dom/test-utils'; import LocationShareMenu from '../../../../src/components/views/location/LocationShareMenu'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; @@ -375,6 +376,16 @@ describe('', () => { describe('Live location share', () => { beforeEach(() => enableSettings(["feature_location_share_live"])); + it('does not display live location share option when composer has a relation', () => { + const relation = { + rel_type: RelationType.Thread, + event_id: '12345', + }; + const component = getComponent({ relation }); + + expect(getShareTypeOption(component, LocationShareType.Live).length).toBeFalsy(); + }); + it('creates beacon info event on submission', () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); From 3174cf2606b4aa00c9de39ce1723d2e2c120f23c Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 3 Jun 2022 12:00:16 +0200 Subject: [PATCH 018/183] Improve widget buttons behaviour and layout (#8734) * Improve widet buttons behaviour and layout Relates to vector-im/element-web#20506 See PSC-79 Signed-off-by: Michael Weimann * Add AppTile tests --- res/css/views/rooms/_AppsDrawer.scss | 57 +++++++++++-------- src/components/views/elements/AppTile.tsx | 31 +++------- src/i18n/strings/en_EN.json | 2 + .../views/elements/AppTile-test.tsx | 49 ++++++++++++++++ 4 files changed, 94 insertions(+), 45 deletions(-) diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 47aa9a9d2ab..0665fc5019a 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -194,8 +194,8 @@ $MinWidth: 240px; align-items: center; justify-content: space-between; width: 100%; - padding-top: 2px; - padding-bottom: 8px; + padding-top: 3px; + padding-bottom: 6px; } .mx_AppTileMenuBarTitle { @@ -221,39 +221,50 @@ $MinWidth: 240px; } .mx_AppTileMenuBar_iconButton { - width: 12px; - height: 12px; - mask-repeat: no-repeat; - mask-position: 0 center; - mask-size: auto 12px; - background-color: $topleftmenu-color; - margin: 0 5px; + height: 24px; + margin: 0 4px; + position: relative; + width: 24px; + + &::before { + background-color: $muted-fg-color; + content: ''; + height: 24px; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 12px; + position: absolute; + width: 24px; + } - &.mx_AppTileMenuBar_iconButton_close { - mask-size: auto 10px; - mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); - background-color: $accent; + &:hover::after { + background-color: $panel-actions; + border-radius: 50%; + content: ''; + height: 24px; + left: 0; + position: absolute; + top: 0; + width: 24px; } - &.mx_AppTileMenuBar_iconButton_maximise { - mask-size: auto 10px; - mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); + &.mx_AppTileMenuBar_iconButton_collapse::before { + mask-image: url("$(res)/img/element-icons/minimise-collapse.svg"); } - &.mx_AppTileMenuBar_iconButton_unpin { - mask-image: url("$(res)/img/element-icons/room/pin-upright.svg"); - background-color: $accent; + &.mx_AppTileMenuBar_iconButton_maximise::before { + mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); } - &.mx_AppTileMenuBar_iconButton_pin { - mask-image: url("$(res)/img/element-icons/room/pin-upright.svg"); + &.mx_AppTileMenuBar_iconButton_minimise::before { + mask-image: url("$(res)/img/element-icons/minus-button.svg"); } - &.mx_AppTileMenuBar_iconButton_popout { + &.mx_AppTileMenuBar_iconButton_popout::before { mask-image: url('$(res)/img/feather-customised/widget/external-link.svg'); } - &.mx_AppTileMenuBar_iconButton_menu { + &.mx_AppTileMenuBar_iconButton_menu::before { mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); } } diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index bdb591fe19a..6f7267b3d8c 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -512,18 +512,14 @@ export default class AppTile extends React.Component { if (!this.props.room) return; // ignore action - it shouldn't even be visible const targetContainer = WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center) - ? Container.Right + ? Container.Top : Container.Center; WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer); }; - private onTogglePinnedClick = (): void => { + private onMinimiseClicked = (): void => { if (!this.props.room) return; // ignore action - it shouldn't even be visible - const targetContainer = - WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Top) - ? Container.Right - : Container.Top; - WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer); + WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, Container.Right); }; private onContextMenuClick = (): void => { @@ -668,32 +664,23 @@ export default class AppTile extends React.Component { isInContainer(this.props.room, this.props.app, Container.Center); const maximisedClasses = classNames({ "mx_AppTileMenuBar_iconButton": true, - "mx_AppTileMenuBar_iconButton_close": isMaximised, + "mx_AppTileMenuBar_iconButton_collapse": isMaximised, "mx_AppTileMenuBar_iconButton_maximise": !isMaximised, }); layoutButtons.push(); - const isPinned = WidgetLayoutStore.instance. - isInContainer(this.props.room, this.props.app, Container.Top); - const pinnedClasses = classNames({ - "mx_AppTileMenuBar_iconButton": true, - "mx_AppTileMenuBar_iconButton_unpin": isPinned, - "mx_AppTileMenuBar_iconButton_pin": !isPinned, - }); layoutButtons.push(); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 71157585d9e..9b5f83c7b8f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2251,6 +2251,8 @@ "Loading...": "Loading...", "Error loading Widget": "Error loading Widget", "Error - Mixed content": "Error - Mixed content", + "Un-maximise": "Un-maximise", + "Minimise": "Minimise", "Popout widget": "Popout widget", "Copy": "Copy", "Share entire screen": "Share entire screen", diff --git a/test/components/views/elements/AppTile-test.tsx b/test/components/views/elements/AppTile-test.tsx index 7850442d32e..207623833fe 100644 --- a/test/components/views/elements/AppTile-test.tsx +++ b/test/components/views/elements/AppTile-test.tsx @@ -19,6 +19,8 @@ import TestRenderer from "react-test-renderer"; import { jest } from "@jest/globals"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixWidgetType } from "matrix-widget-api"; +import { mount, ReactWrapper } from "enzyme"; +import { Optional } from "matrix-events-sdk"; import RightPanel from "../../../../src/components/structures/RightPanel"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -307,4 +309,51 @@ describe("AppTile", () => { await RightPanelStore.instance.onNotReady(); jest.restoreAllMocks(); }); + + describe("for a pinned widget", () => { + let wrapper: ReactWrapper; + let moveToContainerSpy; + + beforeEach(() => { + wrapper = mount(( + + + + )); + + moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, 'moveToContainer'); + }); + + it("clicking 'minimise' should send the widget to the right", () => { + const minimiseButton = wrapper.find('.mx_AppTileMenuBar_iconButton_minimise'); + minimiseButton.first().simulate('click'); + expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Right); + }); + + it("clicking 'maximise' should send the widget to the center", () => { + const minimiseButton = wrapper.find('.mx_AppTileMenuBar_iconButton_maximise'); + minimiseButton.first().simulate('click'); + expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Center); + }); + + describe("for a maximised (centered) widget", () => { + beforeEach(() => { + jest.spyOn(WidgetLayoutStore.instance, 'isInContainer').mockImplementation( + (room: Optional, widget: IApp, container: Container) => { + return room === r1 && widget === app1 && container === Container.Center; + }, + ); + }); + + it("clicking 'un-maximise' should send the widget to the top", () => { + const unMaximiseButton = wrapper.find('.mx_AppTileMenuBar_iconButton_collapse'); + unMaximiseButton.first().simulate('click'); + expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top); + }); + }); + }); }); From ef6bd3540dd9da0f7dd5ba2fe50b872984ab4ac1 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 3 Jun 2022 10:22:13 +0000 Subject: [PATCH 019/183] Revert "Move style rules of MatrixChat_useCompactLayout from _GroupLayout.scss to _EventTile.scss and _RoomView.scss (#8725)" (#8751) This reverts commit 228abb6f076c36ff6b97247c04fe0a4363ad67dc. --- res/css/structures/_RoomView.scss | 4 -- res/css/views/rooms/_EventTile.scss | 80 -------------------------- res/css/views/rooms/_GroupLayout.scss | 82 +++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 84 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index b19173a3139..eba8ae8f6e8 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -272,10 +272,6 @@ hr.mx_RoomView_myReadMarker { .mx_MatrixChat_useCompactLayout { .mx_RoomView_MessageList { margin-bottom: 4px; - - h2 { - margin-top: 6px; // TODO: Use a spacing variable - } } .mx_RoomView_statusAreaBox { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 13eb3c27dfb..3f2a1fbf19c 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -610,86 +610,6 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss } } -/* Compact layout overrides */ - -.mx_MatrixChat_useCompactLayout { - .mx_EventTile { - padding-top: $spacing-4; - - &.mx_EventTile_info { - padding-top: 0; // same as the padding for non-compact .mx_EventTile.mx_EventTile_info - font-size: $font-13px; - - .mx_EventTile_avatar { - top: 4px; - } - - .mx_EventTile_line, - .mx_EventTile_reply { - line-height: $font-20px; - } - } - - &.mx_EventTile_emote { - padding-top: $spacing-8; // add a bit more space for emotes so that avatars don't collide - - &.mx_EventTile_continuation { - padding-top: 0; - - .mx_EventTile_line, - .mx_EventTile_reply { - padding-block: 0; - } - } - - .mx_EventTile_avatar { - top: 2px; - } - - .mx_EventTile_line, - .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 1px; - } - } - - .mx_EventTile_avatar { - top: 2px; - } - - .mx_EventTile_line, - .mx_EventTile_reply { - padding-block: 0; - } - - .mx_EventTile_e2eIcon { - top: 3px; - } - - .mx_DisambiguatedProfile { - font-size: $font-13px; - } - - .mx_ReadReceiptGroup { - // This aligns the avatar with the last line of the - // message. We want to move it one line up - 2rem - top: -2rem; - } - - .mx_EventTile_content .markdown-body { - p, - ul, - ol, - dl, - blockquote, - pre, - table { - margin-bottom: $spacing-4; // 1/4 of the non-compact margin-bottom - } - } - } -} - /* end of overrides */ .mx_EventTile_keyRequestInfo { diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index aab87812540..86c0eb176e1 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -46,3 +46,85 @@ $left-gutter: 64px; } } } + +/* Compact layout overrides */ + +.mx_MatrixChat_useCompactLayout { + .mx_EventTile { + padding-top: 4px; + + .mx_EventTile_line, + .mx_EventTile_reply { + padding-top: 0; + padding-bottom: 0; + } + + &.mx_EventTile_info { + // same as the padding for non-compact .mx_EventTile.mx_EventTile_info + padding-top: 0px; + font-size: $font-13px; + + .mx_EventTile_line, + .mx_EventTile_reply { + line-height: $font-20px; + } + + .mx_EventTile_avatar { + top: 4px; + } + } + + .mx_DisambiguatedProfile { + font-size: $font-13px; + } + + &.mx_EventTile_emote { + // add a bit more space for emotes so that avatars don't collide + padding-top: 8px; + + .mx_EventTile_avatar { + top: 2px; + } + + .mx_EventTile_line, + .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 1px; + } + } + + &.mx_EventTile_emote.mx_EventTile_continuation { + padding-top: 0; + + .mx_EventTile_line, + .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + } + + .mx_EventTile_avatar { + top: 2px; + } + + .mx_EventTile_e2eIcon { + top: 3px; + } + + .mx_ReadReceiptGroup { + // This aligns the avatar with the last line of the + // message. We want to move it one line up - 2rem + top: -2rem; + } + + .mx_EventTile_content .markdown-body { + p, ul, ol, dl, blockquote, pre, table { + margin-bottom: 4px; // 1/4 of the non-compact margin-bottom + } + } + } + + .mx_RoomView_MessageList h2 { + margin-top: 6px; + } +} From 2f7f36ac85e6d503ca4b0016d5d5cb526e3efcaa Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Fri, 3 Jun 2022 08:57:52 -0400 Subject: [PATCH 020/183] Ensure the first device on a newly-registered account gets cross-signed properly (#8750) --- cypress/integration/1-register/register.spec.ts | 5 +++++ .../views/dialogs/security/CreateSecretStorageDialog.tsx | 4 ++-- .../views/dialogs/security/CreateCrossSigningDialog.tsx | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cypress/integration/1-register/register.spec.ts b/cypress/integration/1-register/register.spec.ts index 3dba78c4904..3d710cd965b 100644 --- a/cypress/integration/1-register/register.spec.ts +++ b/cypress/integration/1-register/register.spec.ts @@ -67,5 +67,10 @@ describe("Registration", () => { cy.url().should('contain', '/#/home'); cy.stopMeasuring("from-submit-to-home"); + + cy.get('[aria-label="User menu"]').click(); + cy.get('[aria-label="Security & Privacy"]').click(); + cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon") + .should("have.class", "mx_E2EIcon_verified"); }); }); diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index f58a8b2003d..f310157d28a 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -274,9 +274,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent void): Promise => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => Promise): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { - makeRequest({ + await makeRequest({ type: 'm.login.password', identifier: { type: 'm.id.user', diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index 4b1928c3a73..9cab9a909f2 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -91,9 +91,9 @@ export default class CreateCrossSigningDialog extends React.PureComponent void): Promise => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => Promise): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { - makeRequest({ + await makeRequest({ type: 'm.login.password', identifier: { type: 'm.id.user', @@ -106,7 +106,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent Date: Fri, 3 Jun 2022 13:24:25 -0400 Subject: [PATCH 021/183] Followup type-check fixes for bootstrapCrossSigning callback (#8753) --- .../views/dialogs/security/CreateSecretStorageDialog.tsx | 2 +- .../views/dialogs/security/CreateCrossSigningDialog.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index f310157d28a..466c7dde363 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -274,7 +274,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent Promise): Promise => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => Promise<{}>): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ type: 'm.login.password', diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index 9cab9a909f2..70bf21329fb 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -91,7 +91,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent Promise): Promise => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => Promise<{}>): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ type: 'm.login.password', From 323e911fe72c81f5e96bee5d4da2fac1ad53e2b2 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 5 Jun 2022 05:40:17 +0000 Subject: [PATCH 022/183] Add ellipsis effect to hidden beacon status (#8755) --- res/css/components/views/beacon/_BeaconStatus.scss | 5 +++++ src/components/views/beacon/BeaconStatus.tsx | 14 +++++++++----- .../__snapshots__/BeaconStatus-test.tsx.snap | 8 ++++++-- .../__snapshots__/OwnBeaconStatus-test.tsx.snap | 4 +++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/res/css/components/views/beacon/_BeaconStatus.scss b/res/css/components/views/beacon/_BeaconStatus.scss index 95c41749111..65e51a934ba 100644 --- a/res/css/components/views/beacon/_BeaconStatus.scss +++ b/res/css/components/views/beacon/_BeaconStatus.scss @@ -55,6 +55,11 @@ limitations under the License. white-space: nowrap; overflow: hidden; + + .mx_BeaconStatus_description_status { + text-overflow: ellipsis; + overflow: hidden; + } } .mx_BeaconStatus_expiryTime { diff --git a/src/components/views/beacon/BeaconStatus.tsx b/src/components/views/beacon/BeaconStatus.tsx index 935e22f4f0b..a4a1ca70560 100644 --- a/src/components/views/beacon/BeaconStatus.tsx +++ b/src/components/views/beacon/BeaconStatus.tsx @@ -63,11 +63,15 @@ const BeaconStatus: React.FC> = /> }
- { displayStatus === BeaconDisplayStatus.Loading && { _t('Loading live location...') } } - { displayStatus === BeaconDisplayStatus.Stopped && { _t('Live location ended') } } - - { displayStatus === BeaconDisplayStatus.Error && { _t('Live location error') } } - + { displayStatus === BeaconDisplayStatus.Loading && + { _t('Loading live location...') } + } + { displayStatus === BeaconDisplayStatus.Stopped && + { _t('Live location ended') } + } + { displayStatus === BeaconDisplayStatus.Error && + { _t('Live location error') } + } { displayStatus === BeaconDisplayStatus.Active && beacon && <> <> { label } diff --git a/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap index b3366336a17..b27f9ee191f 100644 --- a/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap @@ -136,7 +136,9 @@ exports[` renders loading state 1`] = `
- + Loading live location...
@@ -165,7 +167,9 @@ exports[` renders stopped state 1`] = `
- + Live location ended
diff --git a/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap b/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap index d2751ba2d9d..fdaa80bdbea 100644 --- a/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap @@ -15,7 +15,9 @@ exports[` renders without a beacon instance 1`] = `
- + Loading live location...
From dc1f53b6e97c092bd09ac5a1d8cb542ad6796be3 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 5 Jun 2022 06:30:29 +0000 Subject: [PATCH 023/183] Prevent the banner text from being selected, replacing the spacing values with the variable (#8756) --- res/css/views/messages/_MImageBody.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 07d0124d431..ba29f06cb03 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -19,12 +19,12 @@ $timeline-image-border-radius: 8px; .mx_MImageBody_banner { position: absolute; - bottom: 4px; - left: 4px; - padding: 4px; + bottom: $spacing-4; + left: $spacing-4; + padding: $spacing-4; border-radius: $timeline-image-border-radius; font-size: $font-15px; - + user-select: none; // prevent banner text from being selected pointer-events: none; // let the cursor go through to the media underneath // Trying to match the width of the image is surprisingly difficult, so arbitrarily break it off early. From 4b5816f5c1cfbb0fb387ee64c62843d35417e76f Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 5 Jun 2022 13:22:44 +0000 Subject: [PATCH 024/183] Make the pill on the basic message composer compatible with display name in RTL languages (#8758) --- res/css/views/elements/_Pill.scss | 4 ++-- res/css/views/rooms/_BasicMessageComposer.scss | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/res/css/views/elements/_Pill.scss b/res/css/views/elements/_Pill.scss index e9ccef666a6..fa854b76628 100644 --- a/res/css/views/elements/_Pill.scss +++ b/res/css/views/elements/_Pill.scss @@ -48,8 +48,8 @@ limitations under the License. &::before, .mx_BaseAvatar { - margin-left: -0.3em; // Otherwise the gap is too large - margin-right: 0.2em; + margin-inline-start: -0.3em; // Otherwise the gap is too large + margin-inline-end: 0.2em; } a& { diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index c89fa6028b6..ea9bcd1adf9 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -51,7 +51,9 @@ limitations under the License. } &.mx_BasicMessageComposer_input_shouldShowPillAvatar { - span.mx_UserPill, span.mx_RoomPill, span.mx_SpacePill { + span.mx_UserPill, + span.mx_RoomPill, + span.mx_SpacePill { user-select: all; position: relative; cursor: unset; // We don't want indicate clickability @@ -66,7 +68,7 @@ limitations under the License. content: var(--avatar-letter); width: $font-16px; height: $font-16px; - margin-right: 0.24rem; + margin-inline-end: 0.24rem; background: var(--avatar-background), $background; color: $avatar-initial-color; background-repeat: no-repeat; From 022535e3891f523b4d167216bf5a25642dd81c92 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 5 Jun 2022 13:38:57 +0000 Subject: [PATCH 025/183] Remove unnecessary ButtonResetDefault from mx_LeftPanelLiveShareWarning (#8761) --- res/css/components/views/beacon/_LeftPanelLiveShareWarning.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/components/views/beacon/_LeftPanelLiveShareWarning.scss b/res/css/components/views/beacon/_LeftPanelLiveShareWarning.scss index 04645c965ed..de7dfa035c8 100644 --- a/res/css/components/views/beacon/_LeftPanelLiveShareWarning.scss +++ b/res/css/components/views/beacon/_LeftPanelLiveShareWarning.scss @@ -15,7 +15,6 @@ limitations under the License. */ .mx_LeftPanelLiveShareWarning { - @mixin ButtonResetDefault; width: 100%; box-sizing: border-box; From 41ee47f8c488c270536fdf004346d48d1180ecbf Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 5 Jun 2022 14:31:13 +0000 Subject: [PATCH 026/183] Tidy up mx_InviteDialog_dialPad style rules (#8762) --- res/css/views/dialogs/_InviteDialog.scss | 66 +++++++++++------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 6420cf61790..6a1f93fdff5 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -345,49 +345,45 @@ limitations under the License. .mx_InviteDialog_helpText { margin: 0; -} - -.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { - padding: 0; -} -.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField { - border-top: 0; - border-left: 0; - border-right: 0; - border-radius: 0; - margin-top: 0; - border-color: $quaternary-content; - - input { - font-size: 18px; - font-weight: 600; - padding-top: 0; + .mx_AccessibleButton_kind_link { + padding: 0; } } -.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField:focus-within { - border-color: $accent; -} - -.mx_InviteDialog_dialPadField .mx_Field_postfix { - /* Remove border separator between postfix and field content */ - border-left: none; -} - .mx_InviteDialog_dialPad { width: 224px; - margin-top: 16px; - margin-left: auto; - margin-right: auto; -} + margin-top: $spacing-16; + margin-inline: auto; + + .mx_InviteDialog_dialPadField { + border-top: 0; + border-inline: 0; + border-radius: 0; + margin-top: 0; + border-color: $quaternary-content; + + &:focus-within { + border-color: $accent; + } -.mx_InviteDialog_dialPad .mx_DialPad { - row-gap: 16px; - column-gap: 48px; + input { + font-size: 18px; + font-weight: 600; + padding-top: 0; + } - margin-left: auto; - margin-right: auto; + .mx_Field_postfix { + /* Remove border separator between postfix and field content */ + border-left: none; + } + } + + .mx_DialPad { + row-gap: $spacing-16; + column-gap: 48px; // TODO: Use a spacing variable + margin-inline: auto; + } } .mx_InviteDialog_transferConsultConnect { From 5ca035772d4b77291eb7bd542953b2f6db2fa149 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 5 Jun 2022 18:44:23 +0000 Subject: [PATCH 027/183] Remove ListResetDefault (#8769) --- res/css/_common.scss | 6 ------ res/css/components/views/beacon/_DialogSidebar.scss | 4 +++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index ade69bc9417..8f315c432fb 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -739,9 +739,3 @@ legend { mask-repeat: no-repeat; mask-size: contain; } - -@define-mixin ListResetDefault { - list-style: none; - padding: 0; - margin: 0; -} diff --git a/res/css/components/views/beacon/_DialogSidebar.scss b/res/css/components/views/beacon/_DialogSidebar.scss index 1989b57c301..af198bd903d 100644 --- a/res/css/components/views/beacon/_DialogSidebar.scss +++ b/res/css/components/views/beacon/_DialogSidebar.scss @@ -53,7 +53,9 @@ limitations under the License. } .mx_DialogSidebar_list { - @mixin ListResetDefault; + list-style: none; + padding: 0; + margin: 0; flex: 1 1 0; width: 100%; overflow: auto; From a6da89481c0f5fc0a8ab906fc6807f661c6e4443 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 6 Jun 2022 04:25:19 +0000 Subject: [PATCH 028/183] Makes the avatar of the user menu non-draggable (#8765) --- res/css/structures/_UserMenu.scss | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index a88e9eddb90..83dab52d9de 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -22,10 +22,14 @@ limitations under the License. .mx_AccessibleButton { display: flex; align-items: center; - } - .mx_UserMenu_userAvatar { - position: relative; + .mx_UserMenu_userAvatar { + position: relative; + + .mx_BaseAvatar { + pointer-events: none; // makes the avatar non-draggable + } + } } .mx_UserMenu_name { From 125a265806aab82c5423003ee22d66132f23b20a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 6 Jun 2022 08:31:20 +0200 Subject: [PATCH 029/183] Improve Typescript in `BasePlatform` (#8768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve Typescript in `BasePlatform` Signed-off-by: Šimon Brandner * `installUpdate()` should not be abstract Signed-off-by: Šimon Brandner --- src/BasePlatform.ts | 95 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 9de1122430b..630d56f06b3 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -63,11 +63,11 @@ export default abstract class BasePlatform { this.startUpdateCheck = this.startUpdateCheck.bind(this); } - abstract getConfig(): Promise; + public abstract getConfig(): Promise; - abstract getDefaultDeviceDisplayName(): string; + public abstract getDefaultDeviceDisplayName(): string; - protected onAction = (payload: ActionPayload) => { + protected onAction = (payload: ActionPayload): void => { switch (payload.action) { case 'on_client_not_viable': case Action.OnLoggedOut: @@ -77,24 +77,24 @@ export default abstract class BasePlatform { }; // Used primarily for Analytics - abstract getHumanReadableName(): string; + public abstract getHumanReadableName(): string; - setNotificationCount(count: number) { + public setNotificationCount(count: number): void { this.notificationCount = count; } - setErrorStatus(errorDidOccur: boolean) { + public setErrorStatus(errorDidOccur: boolean): void { this.errorDidOccur = errorDidOccur; } /** * Whether we can call checkForUpdate on this platform build */ - async canSelfUpdate(): Promise { + public async canSelfUpdate(): Promise { return false; } - startUpdateCheck() { + public startUpdateCheck(): void { hideUpdateToast(); localStorage.removeItem(UPDATE_DEFER_KEY); dis.dispatch({ @@ -107,8 +107,7 @@ export default abstract class BasePlatform { * Update the currently running app to the latest available version * and replace this instance of the app with the new version. */ - installUpdate() { - } + public installUpdate(): void {} /** * Check if the version update has been deferred and that deferment is still in effect @@ -130,7 +129,7 @@ export default abstract class BasePlatform { * Ignore the pending update and don't prompt about this version * until the next morning (8am). */ - deferUpdate(newVersion: string) { + public deferUpdate(newVersion: string): void { const date = new Date(Date.now() + 24 * 60 * 60 * 1000); date.setHours(8, 0, 0, 0); // set to next 8am localStorage.setItem(UPDATE_DEFER_KEY, JSON.stringify([newVersion, date.getTime()])); @@ -141,7 +140,7 @@ export default abstract class BasePlatform { * Return true if platform supports multi-language * spell-checking, otherwise false. */ - supportsMultiLanguageSpellCheck(): boolean { + public supportsMultiLanguageSpellCheck(): boolean { return false; } @@ -157,7 +156,7 @@ export default abstract class BasePlatform { * notifications, otherwise false. * @returns {boolean} whether the platform supports displaying notifications */ - supportsNotifications(): boolean { + public supportsNotifications(): boolean { return false; } @@ -166,7 +165,7 @@ export default abstract class BasePlatform { * to display notifications. Otherwise false. * @returns {boolean} whether the application has permission to display notifications */ - maySendNotifications(): boolean { + public maySendNotifications(): boolean { return false; } @@ -177,7 +176,7 @@ export default abstract class BasePlatform { * that is 'granted' if the user allowed the request or * 'denied' otherwise. */ - abstract requestNotificationPermission(): Promise; + public abstract requestNotificationPermission(): Promise; public displayNotification( title: string, @@ -211,10 +210,9 @@ export default abstract class BasePlatform { return notification; } - loudNotification(ev: MatrixEvent, room: Room) { - } + public loudNotification(ev: MatrixEvent, room: Room): void {} - clearNotification(notif: Notification) { + public clearNotification(notif: Notification): void { // Some browsers don't support this, e.g Safari on iOS // https://developer.mozilla.org/en-US/docs/Web/API/Notification/close if (notif.close) { @@ -225,14 +223,14 @@ export default abstract class BasePlatform { /** * Returns a promise that resolves to a string representing the current version of the application. */ - abstract getAppVersion(): Promise; + public abstract getAppVersion(): Promise; /* * If it's not expected that capturing the screen will work * with getUserMedia, return a string explaining why not. * Otherwise, return null. */ - screenCaptureErrorString(): string { + public screenCaptureErrorString(): string { return "Not implemented"; } @@ -240,54 +238,54 @@ export default abstract class BasePlatform { * Restarts the application, without necessarily reloading * any application code */ - abstract reload(); + public abstract reload(): void; - supportsAutoLaunch(): boolean { + public supportsAutoLaunch(): boolean { return false; } // XXX: Surely this should be a setting like any other? - async getAutoLaunchEnabled(): Promise { + public async getAutoLaunchEnabled(): Promise { return false; } - async setAutoLaunchEnabled(enabled: boolean): Promise { + public async setAutoLaunchEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } - supportsWarnBeforeExit(): boolean { + public supportsWarnBeforeExit(): boolean { return false; } - async shouldWarnBeforeExit(): Promise { + public async shouldWarnBeforeExit(): Promise { return false; } - async setWarnBeforeExit(enabled: boolean): Promise { + public async setWarnBeforeExit(enabled: boolean): Promise { throw new Error("Unimplemented"); } - supportsAutoHideMenuBar(): boolean { + public supportsAutoHideMenuBar(): boolean { return false; } - async getAutoHideMenuBarEnabled(): Promise { + public async getAutoHideMenuBarEnabled(): Promise { return false; } - async setAutoHideMenuBarEnabled(enabled: boolean): Promise { + public async setAutoHideMenuBarEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } - supportsMinimizeToTray(): boolean { + public supportsMinimizeToTray(): boolean { return false; } - async getMinimizeToTrayEnabled(): Promise { + public async getMinimizeToTrayEnabled(): Promise { return false; } - async setMinimizeToTrayEnabled(enabled: boolean): Promise { + public async setMinimizeToTrayEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } @@ -309,23 +307,23 @@ export default abstract class BasePlatform { * @return {BaseEventIndexManager} The EventIndex manager for our platform, * can be null if the platform doesn't support event indexing. */ - getEventIndexingManager(): BaseEventIndexManager | null { + public getEventIndexingManager(): BaseEventIndexManager | null { return null; } - async setLanguage(preferredLangs: string[]) {} + public setLanguage(preferredLangs: string[]) {} - setSpellCheckLanguages(preferredLangs: string[]) {} + public setSpellCheckLanguages(preferredLangs: string[]) {} - getSpellCheckLanguages(): Promise | null { + public getSpellCheckLanguages(): Promise | null { return null; } - async getDesktopCapturerSources(options: GetSourcesOptions): Promise> { + public async getDesktopCapturerSources(options: GetSourcesOptions): Promise> { return []; } - supportsDesktopCapturer(): boolean { + public supportsDesktopCapturer(): boolean { return false; } @@ -335,7 +333,7 @@ export default abstract class BasePlatform { public navigateForwardBack(back: boolean): void {} - getAvailableSpellCheckLanguages(): Promise | null { + public getAvailableSpellCheckLanguages(): Promise | null { return null; } @@ -352,7 +350,12 @@ export default abstract class BasePlatform { * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. * @param {string} idpId The ID of the Identity Provider being targeted, optional. */ - startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string, idpId?: string) { + public startSingleSignOn( + mxClient: MatrixClient, + loginType: "sso" | "cas", + fragmentAfterLogin: string, + idpId?: string, + ): void { // persist hs url and is url for when the user is returned to the app with the login token localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); if (mxClient.getIdentityServerUrl()) { @@ -365,10 +368,6 @@ export default abstract class BasePlatform { window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO } - onKeyDown(ev: KeyboardEvent): boolean { - return false; // no shortcuts implemented - } - /** * Get a previously stored pickle key. The pickle key is used for * encrypting libolm objects. @@ -377,7 +376,7 @@ export default abstract class BasePlatform { * @returns {string|null} the previously stored pickle key, or null if no * pickle key has been stored. */ - async getPickleKey(userId: string, deviceId: string): Promise { + public async getPickleKey(userId: string, deviceId: string): Promise { if (!window.crypto || !window.crypto.subtle) { return null; } @@ -423,7 +422,7 @@ export default abstract class BasePlatform { * @returns {string|null} the pickle key, or null if the platform does not * support storing pickle keys. */ - async createPickleKey(userId: string, deviceId: string): Promise { + public async createPickleKey(userId: string, deviceId: string): Promise { if (!window.crypto || !window.crypto.subtle) { return null; } @@ -462,7 +461,7 @@ export default abstract class BasePlatform { * @param {string} userId the user ID for the user that the pickle key is for. * @param {string} userId the device ID that the pickle key is for. */ - async destroyPickleKey(userId: string, deviceId: string): Promise { + public async destroyPickleKey(userId: string, deviceId: string): Promise { try { await idbDelete("pickleKey", [userId, deviceId]); } catch (e) { From a7f1a0c4f853f7bd7ef362d5e03f2b2a8369a015 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 6 Jun 2022 07:00:16 +0000 Subject: [PATCH 030/183] Fix 'Logout' inline link on the splash screen (#8770) Signed-off-by: Suguru Hirahara --- src/components/structures/MatrixChat.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b143fc74485..c8c59cdcbac 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -2012,9 +2012,11 @@ export default class MatrixChat extends React.PureComponent {
{ errorBox } - - { _t('Logout') } - +
+ + { _t('Logout') } + +
); } From d388ef0e9670736e48225f743cd01895af1e3874 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 6 Jun 2022 10:14:38 +0100 Subject: [PATCH 031/183] Reduce gutter with the new read receipt UI (#8736) --- res/css/views/rooms/_EventTile.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3f2a1fbf19c..6a22bab6fb8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -419,12 +419,12 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss .mx_ThreadSummaryIcon, .mx_EventTile_line { /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ - margin-right: 110px; + margin-right: 80px; min-height: $font-14px; } .mx_ThreadSummary { - max-width: min(calc(100% - $left-gutter - 110px), 600px); // leave space on both left & right gutters + max-width: min(calc(100% - $left-gutter - 80px), 600px); // leave space on both left & right gutters } } From 38b72c4995cb4de435c8bcb6237b7f1b29cef799 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 6 Jun 2022 12:00:18 +0200 Subject: [PATCH 032/183] Fix disappearing widget poput button (#8754) See PSC-79 --- src/components/views/elements/AppTile.tsx | 26 ++++++++--- .../views/elements/AppTile-test.tsx | 43 +++++++++++++++++-- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 6f7267b3d8c..05c67ab9413 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -35,7 +35,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu"; import PersistedElement, { getPersistKey } from "./PersistedElement"; import { WidgetType } from "../../../widgets/WidgetType"; -import { StopGapWidget } from "../../../stores/widgets/StopGapWidget"; +import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions"; import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import WidgetAvatar from "../avatars/WidgetAvatar"; @@ -50,6 +50,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { ActionPayload } from "../../../dispatcher/payloads"; import { Action } from '../../../dispatcher/actions'; import { ElementWidgetCapabilities } from '../../../stores/widgets/ElementWidgetCapabilities'; +import { WidgetMessagingStore } from '../../../stores/widgets/WidgetMessagingStore'; interface IProps { app: IApp; @@ -196,6 +197,24 @@ export default class AppTile extends React.Component { } }; + private determineInitialRequiresClientState(): boolean { + try { + const mockWidget = new ElementWidget(this.props.app); + const widgetApi = WidgetMessagingStore.instance.getMessaging(mockWidget, this.props.room.roomId); + if (widgetApi) { + // Load value from existing API to prevent resetting the requiresClient value on layout changes. + return widgetApi.hasCapability(ElementWidgetCapabilities.RequiresClient); + } + } catch { + // fallback to true + } + + // requiresClient is initially set to true. This avoids the broken state of the popout + // button being visible (for an instance) and then disappearing when the widget is loaded. + // requiresClient <-> hide the popout button + return true; + } + /** * Set initial component state when the App wUrl (widget URL) is being updated. * Component props *must* be passed (rather than relying on this.props). @@ -214,10 +233,7 @@ export default class AppTile extends React.Component { error: null, menuDisplayed: false, widgetPageTitle: this.props.widgetPageTitle, - // requiresClient is initially set to true. This avoids the broken state of the popout - // button being visible (for an instance) and then disappearing when the widget is loaded. - // requiresClient <-> hide the popout button - requiresClient: true, + requiresClient: this.determineInitialRequiresClientState(), }; } diff --git a/test/components/views/elements/AppTile-test.tsx b/test/components/views/elements/AppTile-test.tsx index 207623833fe..d528f52a9ee 100644 --- a/test/components/views/elements/AppTile-test.tsx +++ b/test/components/views/elements/AppTile-test.tsx @@ -18,7 +18,7 @@ import React from "react"; import TestRenderer from "react-test-renderer"; import { jest } from "@jest/globals"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixWidgetType } from "matrix-widget-api"; +import { ClientWidgetApi, MatrixWidgetType } from "matrix-widget-api"; import { mount, ReactWrapper } from "enzyme"; import { Optional } from "matrix-events-sdk"; @@ -39,11 +39,14 @@ import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore"; import AppTile from "../../../../src/components/views/elements/AppTile"; import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore"; import AppsDrawer from "../../../../src/components/views/rooms/AppsDrawer"; +import { ElementWidgetCapabilities } from "../../../../src/stores/widgets/ElementWidgetCapabilities"; +import { ElementWidget } from "../../../../src/stores/widgets/StopGapWidget"; +import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore"; describe("AppTile", () => { let cli; - let r1; - let r2; + let r1: Room; + let r2: Room; const resizeNotifier = new ResizeNotifier(); let app1: IApp; let app2: IApp; @@ -328,6 +331,10 @@ describe("AppTile", () => { moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, 'moveToContainer'); }); + it("requiresClient should be true", () => { + expect(wrapper.state('requiresClient')).toBe(true); + }); + it("clicking 'minimise' should send the widget to the right", () => { const minimiseButton = wrapper.find('.mx_AppTileMenuBar_iconButton_minimise'); minimiseButton.first().simulate('click'); @@ -355,5 +362,35 @@ describe("AppTile", () => { expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top); }); }); + + describe("with an existing widgetApi holding requiresClient = false", () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + const api = { + hasCapability: (capability: ElementWidgetCapabilities): boolean => { + return !(capability === ElementWidgetCapabilities.RequiresClient); + }, + once: () => {}, + } as unknown as ClientWidgetApi; + + const mockWidget = new ElementWidget(app1); + WidgetMessagingStore.instance.storeMessaging(mockWidget, r1.roomId, api); + + wrapper = mount(( + + + + )); + }); + + it("requiresClient should be false", () => { + expect(wrapper.state('requiresClient')).toBe(false); + }); + }); }); }); From 7e244fc83303af5a4206785eaed316938e09a56d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jun 2022 11:37:48 +0100 Subject: [PATCH 033/183] Switch to composite actions for pr_details and sonarqube (#8729) * Switch to composite actions for pr_details and sonarqube * Bring back a reusable workflow for element-web stack sonarqube runs * Move sonarcloud.yml to the right repo * Fix Netlify run --- .github/workflows/netlify.yaml | 18 ++++++++---------- .github/workflows/sonarqube.yml | 22 ---------------------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index b8298c79247..3b7b57c2276 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -7,16 +7,8 @@ on: types: - completed jobs: - prdetails: - name: ℹ️ PR Details - if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' - uses: matrix-org/matrix-js-sdk/.github/workflows/pr_details.yml@develop - with: - owner: ${{ github.event.workflow_run.head_repository.owner.login }} - branch: ${{ github.event.workflow_run.head_branch }} - deploy: - needs: prdetails + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: - name: 📝 Create Deployment @@ -31,6 +23,12 @@ jobs: Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. + - id: prdetails + uses: matrix-org/pr-details-action@v1.1 + with: + owner: ${{ github.event.workflow_run.head_repository.owner.login }} + branch: ${{ github.event.workflow_run.head_branch }} + # 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 @@ -50,7 +48,7 @@ jobs: # These don't work because we're in workflow_run enable-pull-request-comment: false enable-commit-comment: false - alias: pr${{ needs.prdetails.outputs.pr_id }} + alias: pr${{ steps.prdetails.outputs.pr_id }} env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 11660e68ba4..a5360c64fbb 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -8,30 +8,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} cancel-in-progress: true jobs: - prdetails: - name: ℹ️ PR Details - if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' - uses: matrix-org/matrix-js-sdk/.github/workflows/pr_details.yml@develop - with: - owner: ${{ github.event.workflow_run.head_repository.owner.login }} - branch: ${{ github.event.workflow_run.head_branch }} - sonarqube: name: 🩻 SonarQube - needs: prdetails - # Only wait for prdetails if it isn't skipped - if: | - always() && - (needs.prdetails.result == 'success' || needs.prdetails.result == 'skipped') && - github.event.workflow_run.conclusion == 'success' uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop - with: - repo: ${{ github.event.workflow_run.head_repository.full_name }} - pr_id: ${{ needs.prdetails.outputs.pr_id }} - head_branch: ${{ needs.prdetails.outputs.head_branch || github.event.workflow_run.head_branch }} - base_branch: ${{ needs.prdetails.outputs.base_branch }} - revision: ${{ github.event.workflow_run.head_sha }} - coverage_workflow_name: tests.yml - coverage_run_id: ${{ github.event.workflow_run.id }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 027827393a764d9dbfdd4f57c364a443a3c0e789 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jun 2022 13:47:40 -0500 Subject: [PATCH 034/183] Add more debug logging to try and find out why ring and ringback sounds aren't playing (#8772) To better investigate https://github.com/vector-im/element-web/issues/20832 --- src/CallHandler.tsx | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index c6b28d65c44..c64bedf8920 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -375,6 +375,8 @@ export default class CallHandler extends EventEmitter { } public play(audioId: AudioID): void { + const logPrefix = `CallHandler.play(${audioId}):`; + logger.debug(`${logPrefix} beginning of function`); // TODO: Attach an invisible element for this instead // which listens? const audio = document.getElementById(audioId) as HTMLMediaElement; @@ -383,13 +385,15 @@ export default class CallHandler extends EventEmitter { try { // This still causes the chrome debugger to break on promise rejection if // the promise is rejected, even though we're catching the exception. + logger.debug(`${logPrefix} attempting to play audio`); await audio.play(); + logger.debug(`${logPrefix} playing audio successfully`); } catch (e) { // This is usually because the user hasn't interacted with the document, // or chrome doesn't think so and is denying the request. Not sure what // we can really do here... // https://github.com/vector-im/element-web/issues/7657 - logger.log("Unable to play audio clip", e); + logger.warn(`${logPrefix} unable to play audio clip`, e); } }; if (this.audioPromises.has(audioId)) { @@ -400,20 +404,30 @@ export default class CallHandler extends EventEmitter { } else { this.audioPromises.set(audioId, playAudio()); } + } else { + logger.warn(`${logPrefix} unable to find