From 1c1d5ea5a90dc3606e4901b1f71af22998700b0a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Apr 2022 12:07:43 +0100 Subject: [PATCH 001/102] [Release] Fixes around threads beta in degraded mode (#8319) * Fix soft crash around the thread panel in degraded mode * Better specify fallback key * Hide MAB Threads prompt if user would have degraded mode * Confirm user wants to enable Threads beta if in degraded mode * Fix copy --- src/components/structures/ThreadPanel.tsx | 2 +- .../views/messages/MessageActionBar.tsx | 6 +++ src/i18n/strings/en_EN.json | 4 ++ src/settings/Settings.tsx | 5 +- src/settings/SettingsStore.ts | 9 ++-- src/settings/controllers/SettingController.ts | 11 ++++ .../controllers/ThreadBetaController.tsx | 53 +++++++++++++++++++ 7 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/settings/controllers/ThreadBetaController.tsx diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 7d75f918589..bc2c53d2035 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -286,7 +286,7 @@ const ThreadPanel: React.FC = ({ /> { timelineSet && ( Learn more.": "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.", + "Do you want to enable threads anyway?": "Do you want to enable threads anyway?", + "Yes, enable": "Yes, enable", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 8cd842b74e5..bd34297161b 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -42,6 +42,7 @@ import IncompatibleController from "./controllers/IncompatibleController"; import { ImageSize } from "./enums/ImageSize"; import { MetaSpace } from "../stores/spaces"; import SdkConfig from "../SdkConfig"; +import ThreadBetaController from './controllers/ThreadBetaController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -222,9 +223,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { "feature_thread": { isFeature: true, labsGroup: LabGroup.Messaging, - // Requires a reload as we change an option flag on the `js-sdk` - // And the entire sync history needs to be parsed again - controller: new ReloadOnChangeController(), + controller: new ThreadBetaController(), displayName: _td("Threaded messaging"), supportedLevels: LEVELS_FEATURE, default: false, diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index ca9bfe3703e..95ea0e6993e 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -451,12 +451,13 @@ export default class SettingsStore { throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId); } + if (setting.controller && !(await setting.controller.beforeChange(level, roomId, value))) { + return; // controller says no + } + await handler.setValue(settingName, roomId, value); - const controller = setting.controller; - if (controller) { - controller.onChange(level, roomId, value); - } + setting.controller?.onChange(level, roomId, value); } /** diff --git a/src/settings/controllers/SettingController.ts b/src/settings/controllers/SettingController.ts index 292b2d63e59..a274bcff2c3 100644 --- a/src/settings/controllers/SettingController.ts +++ b/src/settings/controllers/SettingController.ts @@ -46,6 +46,17 @@ export default abstract class SettingController { return null; // no override } + /** + * Called before the setting value has been changed, can abort the change. + * @param {string} level The level at which the setting has been modified. + * @param {String} roomId The room ID, may be null. + * @param {*} newValue The new value for the setting, may be null. + * @return {boolean} Whether the settings change should be accepted. + */ + public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise { + return true; + } + /** * Called when the setting value has been changed. * @param {string} level The level at which the setting has been modified. diff --git a/src/settings/controllers/ThreadBetaController.tsx b/src/settings/controllers/ThreadBetaController.tsx new file mode 100644 index 00000000000..487d2013b17 --- /dev/null +++ b/src/settings/controllers/ThreadBetaController.tsx @@ -0,0 +1,53 @@ +/* +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 * as React from "react"; +import { Thread } from "matrix-js-sdk/src/models/thread"; + +import SettingController from "./SettingController"; +import PlatformPeg from "../../PlatformPeg"; +import { SettingLevel } from "../SettingLevel"; +import Modal from "../../Modal"; +import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; +import { _t } from "../../languageHandler"; + +export default class ThreadBetaController extends SettingController { + public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise { + if (Thread.hasServerSideSupport || !newValue) return true; // Full support or user is disabling + + const { finished } = Modal.createTrackedDialog<[boolean]>("Thread beta", "degraded mode", QuestionDialog, { + title: _t("Partial Support for Threads"), + description: <> +

{ _t("Your homeserver does not currently support threads, so this feature may be unreliable. " + + "Some threaded messages may not be reliably available. Learn more.", {}, { + a: sub => ( + { sub } + ), + }) }

+

{ _t("Do you want to enable threads anyway?") }

+ , + button: _t("Yes, enable"), + }); + const [enable] = await finished; + return enable; + } + + public onChange(level: SettingLevel, roomId: string, newValue: any) { + // Requires a reload as we change an option flag on the `js-sdk` + // And the entire sync history needs to be parsed again + PlatformPeg.get().reload(); + } +} From b91429fdf995a0cf4ac68757157aa9fb23d69a03 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 14 Apr 2022 13:56:13 +0100 Subject: [PATCH 002/102] Prepare changelog for v3.42.4 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0126f1f369f..62f25872eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [3.42.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.42.4) (2022-04-14) +===================================================================================================== + +## 🐛 Bug Fixes + * Fixes around threads beta in degraded mode ([\#8319](https://github.com/matrix-org/matrix-react-sdk/pull/8319)). Fixes vector-im/element-web#21762. + Changes in [3.42.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.42.3) (2022-04-12) ===================================================================================================== From b6155e56c09ce488a3b40524a79a89395323b317 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 14 Apr 2022 13:56:13 +0100 Subject: [PATCH 003/102] v3.42.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8293d4198db..82f4116272c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.42.3", + "version": "3.42.4", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 4fb9cd655091bb8c93c6bb472f1ad0759dc1edea Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 19 Apr 2022 08:46:57 +0200 Subject: [PATCH 004/102] calculate geo bounds (#8321) Signed-off-by: Kerry Archibald --- src/utils/beacon/bounds.ts | 56 +++++++++++++++++++ test/utils/beacon/bounds-test.ts | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 src/utils/beacon/bounds.ts create mode 100644 test/utils/beacon/bounds-test.ts diff --git a/src/utils/beacon/bounds.ts b/src/utils/beacon/bounds.ts new file mode 100644 index 00000000000..43c063b1c55 --- /dev/null +++ b/src/utils/beacon/bounds.ts @@ -0,0 +1,56 @@ +/* +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 { Beacon } from "matrix-js-sdk/src/matrix"; + +import { parseGeoUri } from "../location"; + +export type Bounds = { + north: number; + east: number; + west: number; + south: number; +}; + +/** + * Get the geo bounds of given list of beacons + * + * Latitude: + * equator: 0, North pole: 90, South pole -90 + * Longitude: + * Prime Meridian (Greenwich): 0 + * east of Greenwich has a positive longitude, max 180 + * west of Greenwich has a negative longitude, min -180 + */ +export const getBeaconBounds = (beacons: Beacon[]): Bounds | undefined => { + const coords = beacons.filter(beacon => !!beacon.latestLocationState) + .map(beacon => parseGeoUri(beacon.latestLocationState.uri)); + + if (!coords.length) { + return; + } + + // sort descending + const sortedByLat = [...coords].sort((left, right) => right.latitude - left.latitude); + const sortedByLong = [...coords].sort((left, right) => right.longitude - left.longitude); + + return { + north: sortedByLat[0].latitude, + south: sortedByLat[sortedByLat.length - 1].latitude, + east: sortedByLong[0].longitude, + west: sortedByLong[sortedByLong.length -1].longitude, + }; +}; diff --git a/test/utils/beacon/bounds-test.ts b/test/utils/beacon/bounds-test.ts new file mode 100644 index 00000000000..bd4b37234b0 --- /dev/null +++ b/test/utils/beacon/bounds-test.ts @@ -0,0 +1,95 @@ +/* +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 { Beacon } from "matrix-js-sdk/src/matrix"; + +import { Bounds, getBeaconBounds } from "../../../src/utils/beacon/bounds"; +import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils"; + +describe('getBeaconBounds()', () => { + const userId = '@user:server'; + const roomId = '!room:server'; + const makeBeaconWithLocation = (latLon: {lat: number, lon: number}) => { + const geoUri = `geo:${latLon.lat},${latLon.lon}`; + const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true })); + // @ts-ignore private prop, sets internal live property so addLocations works + beacon.checkLiveness(); + const location = makeBeaconEvent(userId, { + beaconInfoId: beacon.beaconInfoId, + geoUri, + timestamp: Date.now() + 1, + }); + beacon.addLocations([location]); + + return beacon; + }; + + const geo = { + // northern hemi + // west of greenwich + london: { lat: 51.5, lon: -0.14 }, + reykjavik: { lat: 64.08, lon: -21.82 }, + // east of greenwich + paris: { lat: 48.85, lon: 2.29 }, + // southern hemi + // east + auckland: { lat: -36.85, lon: 174.76 }, // nz + // west + lima: { lat: -12.013843, lon: -77.008388 }, // peru + }; + + const london = makeBeaconWithLocation(geo.london); + const reykjavik = makeBeaconWithLocation(geo.reykjavik); + const paris = makeBeaconWithLocation(geo.paris); + const auckland = makeBeaconWithLocation(geo.auckland); + const lima = makeBeaconWithLocation(geo.lima); + + it('should return undefined when there are no beacons', () => { + expect(getBeaconBounds([])).toBeUndefined(); + }); + + it('should return undefined when no beacons have locations', () => { + const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId)); + expect(getBeaconBounds([beacon])).toBeUndefined(); + }); + + type TestCase = [string, Beacon[], Bounds]; + it.each([ + ['one beacon', [london], + { north: geo.london.lat, south: geo.london.lat, east: geo.london.lon, west: geo.london.lon }, + ], + ['beacons in the northern hemisphere, west of meridian', + [london, reykjavik], + { north: geo.reykjavik.lat, south: geo.london.lat, east: geo.london.lon, west: geo.reykjavik.lon }, + ], + ['beacons in the northern hemisphere, both sides of meridian', + [london, reykjavik, paris], + // reykjavik northmost and westmost, paris southmost and eastmost + { north: geo.reykjavik.lat, south: geo.paris.lat, east: geo.paris.lon, west: geo.reykjavik.lon }, + ], + ['beacons in the southern hemisphere', + [auckland, lima], + // lima northmost and westmost, auckland southmost and eastmost + { north: geo.lima.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon }, + ], + ['beacons in both hemispheres', + [auckland, lima, paris], + { north: geo.paris.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon }, + ], + ])('gets correct bounds for %s', (_description, beacons, expectedBounds) => { + expect(getBeaconBounds(beacons)).toEqual(expectedBounds); + }); +}); From 2f3249793f8dfa14398fddc9ad78b0f9d66848bb Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 19 Apr 2022 08:55:35 +0200 Subject: [PATCH 005/102] use unstable prefix for wk tile_Server (#8316) Signed-off-by: Kerry Archibald From 511965b8409d2b00e6ed2371a1c121f869664665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 19 Apr 2022 09:59:23 +0200 Subject: [PATCH 006/102] Cleanup `MessageContextMenu` code (#8351) --- .../context_menus/MessageContextMenu.tsx | 72 ++++++------------- .../views/messages/MessageEvent.tsx | 6 +- src/components/views/rooms/EventTile.tsx | 7 +- src/utils/EventUtils.ts | 19 +++++ 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index f77a0dd41e8..55b893b784d 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -21,7 +21,6 @@ import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import { Relations } from 'matrix-js-sdk/src/models/relations'; import { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member"; -import { M_LOCATION } from 'matrix-js-sdk/src/@types/location'; import { M_POLL_START } from "matrix-events-sdk"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -31,7 +30,7 @@ import Modal from '../../../Modal'; import Resend from '../../../Resend'; import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; -import { canEditContent, editEvent, isContentActionable } from '../../../utils/EventUtils'; +import { canEditContent, canForward, editEvent, isContentActionable, isLocationEvent } from '../../../utils/EventUtils'; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu'; import { ReadPinsEventId } from "../right_panel/types"; import { Action } from "../../../dispatcher/actions"; @@ -49,20 +48,11 @@ import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInse import EndPollDialog from '../dialogs/EndPollDialog'; import { isPollEnded } from '../messages/MPollBody'; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { GetRelationsForEvent } from "../rooms/EventTile"; +import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload"; import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload"; import { createMapSiteLink } from '../../../utils/location'; -export interface IEventTileOps { - isWidgetHidden(): boolean; - unhideWidget(): void; -} - -export interface IOperableEventTile { - getEventTileOps(): IEventTileOps; -} - interface IProps extends IPosition { chevronFace: ChevronFace; /* the MatrixEvent associated with the context menu */ @@ -343,28 +333,7 @@ export default class MessageContextMenu extends React.Component ); const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent; - let openInMapSiteButton: JSX.Element; - let endPollButton: JSX.Element; let resendReactionsButton: JSX.Element; - let redactButton: JSX.Element; - let forwardButton: JSX.Element; - let pinButton: JSX.Element; - let unhidePreviewButton: JSX.Element; - let externalURLButton: JSX.Element; - let quoteButton: JSX.Element; - let redactItemList: JSX.Element; - let reportEventButton: JSX.Element; - let copyButton: JSX.Element; - let editButton: JSX.Element; - let replyButton: JSX.Element; - let reactButton: JSX.Element; - let reactionPicker: JSX.Element; - let quickItemsList: JSX.Element; - let nativeItemsList: JSX.Element; - let permalinkButton: JSX.Element; - let collapseReplyChainButton: JSX.Element; - let viewInRoomButton: JSX.Element; - if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) { resendReactionsButton = ( ); } + let redactButton: JSX.Element; if (isSent && this.state.canRedact) { redactButton = ( ); } + let openInMapSiteButton: JSX.Element; if (this.canOpenInMapSite(mxEvent)) { const mapSiteLink = createMapSiteLink(mxEvent); openInMapSiteButton = ( @@ -404,6 +375,7 @@ export default class MessageContextMenu extends React.Component ); } + let forwardButton: JSX.Element; if (contentActionable && canForward(mxEvent)) { forwardButton = ( ); } + let pinButton: JSX.Element; if (contentActionable && this.state.canPin) { pinButton = ( ); } + let unhidePreviewButton: JSX.Element; if (eventTileOps?.isWidgetHidden()) { unhidePreviewButton = ( ); } + let permalinkButton: JSX.Element; if (permalink) { permalinkButton = ( ); } + let endPollButton: JSX.Element; if (this.canEndPoll(mxEvent)) { endPollButton = ( ); } + let quoteButton: JSX.Element; if (eventTileOps) { // this event is rendered using TextualBody quoteButton = ( } // Bridges can provide a 'external_url' to link back to the source. + let externalURLButton: JSX.Element; if ( typeof (mxEvent.getContent().external_url) === "string" && isUrlPermitted(mxEvent.getContent().external_url) @@ -511,6 +489,7 @@ export default class MessageContextMenu extends React.Component ); } + let collapseReplyChainButton: JSX.Element; if (collapseReplyChain) { collapseReplyChainButton = ( ); } + let reportEventButton: JSX.Element; if (mxEvent.getSender() !== me) { reportEventButton = ( ); } + let copyButton: JSX.Element; if (rightClick && getSelectedText()) { copyButton = ( ); } + let editButton: JSX.Element; if (rightClick && canEditContent(mxEvent)) { editButton = ( ); } + let replyButton: JSX.Element; if (rightClick && contentActionable && canSendMessages) { replyButton = ( ); } + let reactButton; if (rightClick && contentActionable && canReact) { reactButton = ( ); } + let viewInRoomButton: JSX.Element; if (isThreadRootEvent) { viewInRoomButton = ( ); } + let nativeItemsList: JSX.Element; if (copyButton) { nativeItemsList = ( @@ -591,6 +577,7 @@ export default class MessageContextMenu extends React.Component ); } + let quickItemsList: JSX.Element; if (editButton || replyButton || reactButton) { quickItemsList = ( @@ -619,6 +606,7 @@ export default class MessageContextMenu extends React.Component ); + let redactItemList: JSX.Element; if (redactButton) { redactItemList = ( @@ -627,6 +615,7 @@ export default class MessageContextMenu extends React.Component ); } + let reactionPicker: JSX.Element; if (this.state.reactionPickerDisplayed) { const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect(); reactionPicker = ( @@ -662,20 +651,3 @@ export default class MessageContextMenu extends React.Component } } -function canForward(event: MatrixEvent): boolean { - return !( - isLocationEvent(event) || - M_POLL_START.matches(event.getType()) - ); -} - -function isLocationEvent(event: MatrixEvent): boolean { - const eventType = event.getType(); - return ( - M_LOCATION.matches(eventType) || - ( - eventType === EventType.RoomMessage && - M_LOCATION.matches(event.getContent().msgtype) - ) - ); -} diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 2bafadf5178..d52629b56d6 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -26,7 +26,6 @@ import { Mjolnir } from "../../../mjolnir/Mjolnir"; import RedactedBody from "./RedactedBody"; import UnknownBody from "./UnknownBody"; import { IMediaBody } from "./IMediaBody"; -import { IOperableEventTile } from "../context_menus/MessageContextMenu"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { ReactAnyComponent } from "../../../@types/common"; import { IBodyProps } from "./IBodyProps"; @@ -41,6 +40,7 @@ import MPollBody from "./MPollBody"; import MLocationBody from "./MLocationBody"; import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; +import { IEventTileOps } from "../rooms/EventTile"; // onMessageAllowed is handled internally interface IProps extends Omit { @@ -54,6 +54,10 @@ interface IProps extends Omit implements IMediaBody, IOperableEventTile { private body: React.RefObject = createRef(); private mediaHelper: MediaEventHelper; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 9d5dc2cefe1..4c68ee532e8 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -38,7 +38,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { E2EState } from "./E2EIcon"; import { toRem } from "../../../utils/units"; import RoomAvatar from "../avatars/RoomAvatar"; -import MessageContextMenu, { IEventTileOps } from "../context_menus/MessageContextMenu"; +import MessageContextMenu from "../context_menus/MessageContextMenu"; import { aboveRightOf } from '../../structures/ContextMenu'; import { objectHasDiff } from "../../../utils/objects"; import Tooltip from "../elements/Tooltip"; @@ -99,6 +99,11 @@ export interface IReadReceiptProps { ts: number; } +export interface IEventTileOps { + isWidgetHidden(): boolean; + unhideWidget(): void; +} + export interface IEventTileType extends React.Component { getEventTileOps?(): IEventTileOps; } diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 3aef3eb369e..51ab6cbed06 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -19,6 +19,7 @@ import { EventType, EVENT_VISIBILITY_CHANGE_TYPE, MsgType, RelationType } from " import { MatrixClient } from 'matrix-js-sdk/src/client'; import { logger } from 'matrix-js-sdk/src/logger'; import { M_POLL_START } from "matrix-events-sdk"; +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; import { MatrixClientPeg } from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; @@ -262,3 +263,21 @@ export function editEvent( export function canCancel(status: EventStatus): boolean { return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING; } + +export const isLocationEvent = (event: MatrixEvent): boolean => { + const eventType = event.getType(); + return ( + M_LOCATION.matches(eventType) || + ( + eventType === EventType.RoomMessage && + M_LOCATION.matches(event.getContent().msgtype) + ) + ); +}; + +export function canForward(event: MatrixEvent): boolean { + return !( + isLocationEvent(event) || + M_POLL_START.matches(event.getType()) + ); +} From 2b91ed10849b1b44f146227c16999606c01b0b77 Mon Sep 17 00:00:00 2001 From: olivialivia <94136530+olivialivia@users.noreply.github.com> Date: Tue, 19 Apr 2022 09:39:34 +0100 Subject: [PATCH 007/102] Add copy buttons for event & room ID (#8302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Šimon Brandner --- res/css/views/elements/_CopyableText.scss | 13 ++++++++----- src/components/structures/ViewSource.tsx | 12 ++++++++++-- src/components/views/elements/CopyableText.tsx | 10 ++++++++-- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/res/css/views/elements/_CopyableText.scss b/res/css/views/elements/_CopyableText.scss index a08306b66a4..ceafd422730 100644 --- a/res/css/views/elements/_CopyableText.scss +++ b/res/css/views/elements/_CopyableText.scss @@ -18,14 +18,17 @@ limitations under the License. .mx_CopyableText { display: flex; justify-content: space-between; - border-radius: 5px; - border: solid 1px $light-fg-color; - margin-bottom: 10px; - margin-top: 10px; - padding: 10px; width: max-content; max-width: 100%; + &.mx_CopyableText_border { + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; + } + .mx_CopyableText_copyButton { flex-shrink: 0; width: 20px; diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index e84a26409e2..c8628a7f96f 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -167,8 +167,16 @@ export default class ViewSource extends React.Component { return (
-
{ _t("Room ID: %(roomId)s", { roomId }) }
-
{ _t("Event ID: %(eventId)s", { eventId }) }
+
+ roomId} border={false}> + { _t("Room ID: %(roomId)s", { roomId }) } + +
+
+ eventId} border={false}> + { _t("Event ID: %(eventId)s", { eventId }) } + +
{ isEditing ? this.editSourceContent() : this.viewSourceContent() }
diff --git a/src/components/views/elements/CopyableText.tsx b/src/components/views/elements/CopyableText.tsx index d1632af3825..4ed3a7a58c1 100644 --- a/src/components/views/elements/CopyableText.tsx +++ b/src/components/views/elements/CopyableText.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React, { useState } from "react"; +import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { copyPlaintext } from "../../../utils/strings"; @@ -25,9 +26,10 @@ import AccessibleTooltipButton from "./AccessibleTooltipButton"; interface IProps { children: React.ReactNode; getTextToCopy: () => string; + border?: boolean; } -const CopyableText: React.FC = ({ children, getTextToCopy }) => { +const CopyableText: React.FC = ({ children, getTextToCopy, border=true }) => { const [tooltip, setTooltip] = useState(undefined); const onCopyClickInternal = async (e: ButtonEvent) => { @@ -42,7 +44,11 @@ const CopyableText: React.FC = ({ children, getTextToCopy }) => { } }; - return
+ const className = classNames("mx_CopyableText", { + mx_CopyableText_border: border, + }); + + return
{ children } Date: Tue, 19 Apr 2022 08:43:35 +0000 Subject: [PATCH 008/102] Replace "Reply in thread" and "Leave the beta" with variables (#8349) --- src/components/structures/ThreadPanel.tsx | 4 +++- src/i18n/strings/en_EN.json | 10 +++++----- src/settings/Settings.tsx | 12 ++++++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 1dd37a8c412..3009bedae3e 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -163,7 +163,9 @@ const EmptyThread: React.FC = ({ hasThreads, filterOption, sh body = <>

{ _t("Threads help keep your conversations on-topic and easy to track.") }

- { _t('Tip: Use "Reply in thread" when hovering over a message.', {}, { + { _t('Tip: Use “%(replyInThread)s” when hovering over a message.', { + replyInThread: _t("Reply in thread"), + }, { b: sub => { sub }, }) }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c5ecce8e81a..aacc7f577d9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -870,9 +870,11 @@ "Keep discussions organised with threads.": "Keep discussions organised with threads.", "Threads help keep conversations on-topic and easy to track. Learn more.": "Threads help keep conversations on-topic and easy to track. Learn more.", "How can I start a thread?": "How can I start a thread?", - "Use \"Reply in thread\" when hovering over a message.": "Use \"Reply in thread\" when hovering over a message.", + "Use “%(replyInThread)s” when hovering over a message.": "Use “%(replyInThread)s” when hovering over a message.", + "Reply in thread": "Reply in thread", "How can I leave the beta?": "How can I leave the beta?", - "To leave, return to this page and use the “Leave the beta” button.": "To leave, return to this page and use the “Leave the beta” button.", + "To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.", + "Leave the beta": "Leave the beta", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", "Custom user status messages": "Custom user status messages", "Video rooms (under active development)": "Video rooms (under active development)", @@ -2090,7 +2092,6 @@ "Error processing audio message": "Error processing audio message", "View live location": "View live location", "React": "React", - "Reply in thread": "Reply in thread", "Can't create a thread from an event with an existing relation": "Can't create a thread from an event with an existing relation", "Beta feature": "Beta feature", "Beta feature. Click to learn more.": "Beta feature. Click to learn more.", @@ -2909,7 +2910,6 @@ "This is a beta feature": "This is a beta feature", "Click for more info": "Click for more info", "Beta": "Beta", - "Leave the beta": "Leave the beta", "Join the beta": "Join the beta", "Live until %(expiryTime)s": "Live until %(expiryTime)s", "Loading live location...": "Loading live location...", @@ -3124,7 +3124,7 @@ "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.", "Show all threads": "Show all threads", "Threads help keep your conversations on-topic and easy to track.": "Threads help keep your conversations on-topic and easy to track.", - "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Use \"Reply in thread\" when hovering over a message.", + "Tip: Use “%(replyInThread)s” when hovering over a message.": "Tip: Use “%(replyInThread)s” when hovering over a message.", "Keep discussions organised with threads": "Keep discussions organised with threads", "Threads are a beta feature": "Threads are a beta feature", "Give feedback": "Give feedback", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 559558cee88..fa773080be2 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -242,9 +242,17 @@ export const SETTINGS: {[setting: string]: ISetting} = { disclaimer: () => SdkConfig.get().bug_report_endpoint_url && <>

{ _t("How can I start a thread?") }

-

{ _t("Use \"Reply in thread\" when hovering over a message.") }

+

+ { _t("Use “%(replyInThread)s” when hovering over a message.", { + replyInThread: _t("Reply in thread"), + }) } +

{ _t("How can I leave the beta?") }

-

{ _t("To leave, return to this page and use the “Leave the beta” button.") }

+

+ { _t("To leave, return to this page and use the “%(leaveTheBeta)s” button.", { + leaveTheBeta: _t("Leave the beta"), + }) } +

, feedbackLabel: "thread-feedback", feedbackSubheading: _td("Thank you for trying the beta, " + From 949b3cc6508d6c33b4417dfac90dcdb154ac692c Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 19 Apr 2022 09:52:20 +0100 Subject: [PATCH 009/102] Extract Tag to its own component (#8309) --- res/css/views/elements/_TagComposer.scss | 73 +++++++++++-------- src/components/views/elements/Tag.tsx | 44 +++++++++++ src/components/views/elements/TagComposer.tsx | 12 ++- 3 files changed, 94 insertions(+), 35 deletions(-) create mode 100644 src/components/views/elements/Tag.tsx diff --git a/res/css/views/elements/_TagComposer.scss b/res/css/views/elements/_TagComposer.scss index f5bdb8d2d58..7efe83281f5 100644 --- a/res/css/views/elements/_TagComposer.scss +++ b/res/css/views/elements/_TagComposer.scss @@ -29,7 +29,9 @@ limitations under the License. margin-left: 16px; // distance from } - .mx_Field, .mx_Field input, .mx_AccessibleButton { + .mx_Field, + .mx_Field input, + .mx_AccessibleButton { // So they look related to each other by feeling the same border-radius: 8px; } @@ -39,39 +41,48 @@ limitations under the License. display: flex; flex-wrap: wrap; margin-top: 12px; // this plus 12px from the tags makes 24px from the input + } - .mx_TagComposer_tag { - padding: 6px 8px 8px 12px; - position: relative; - margin-right: 12px; - margin-top: 12px; - - // Cheaty way to get an opacified variable colour background - &::before { - content: ''; - border-radius: 20px; - background-color: $tertiary-content; - opacity: 0.15; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - - // Pass through the pointer otherwise we have effectively put a whole div - // on top of the component, which makes it hard to interact with buttons. - pointer-events: none; - } - } + .mx_Tag { + margin-right: 12px; + margin-top: 12px; + } +} - .mx_AccessibleButton { - background-image: url('$(res)/img/subtract.svg'); - width: 16px; - height: 16px; - margin-left: 8px; - display: inline-block; +.mx_Tag { + + font-size: $font-15px; + + display: inline-flex; + align-items: center; + + gap: 8px; + padding: 8px; + border-radius: 8px; + + color: $primary-content; + background: $quinary-content; + + >svg:first-child { + width: 1em; + color: $secondary-content; + transform: scale(1.25); + transform-origin: center; + } + + .mx_Tag_delete { + border-radius: 50%; + text-align: center; + width: 1.066666em; // 16px; + height: 1.066666em; + line-height: 1em; + color: $secondary-content; + background: $system; + position: relative; + + svg { + width: .5em; vertical-align: middle; - cursor: pointer; } } } diff --git a/src/components/views/elements/Tag.tsx b/src/components/views/elements/Tag.tsx new file mode 100644 index 00000000000..c6887fd0e0a --- /dev/null +++ b/src/components/views/elements/Tag.tsx @@ -0,0 +1,44 @@ +/* +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 AccessibleButton from "./AccessibleButton"; +import { Icon as CancelRounded } from "../../../../res/img/element-icons/cancel-rounded.svg"; + +interface IProps { + icon?: () => JSX.Element; + label: string; + onDeleteClick?: () => void; + disabled?: boolean; +} + +export const Tag = ({ + icon, + label, + onDeleteClick, + disabled = false, +}: IProps) => { + return
+ { icon?.() } + { label } + { onDeleteClick && ( + + + + ) } +
; +}; diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx index 19f3523f067..5d1bff84e8e 100644 --- a/src/components/views/elements/TagComposer.tsx +++ b/src/components/views/elements/TagComposer.tsx @@ -19,6 +19,7 @@ import React, { ChangeEvent, FormEvent } from "react"; import Field from "./Field"; import { _t } from "../../../languageHandler"; import AccessibleButton from "./AccessibleButton"; +import { Tag } from "./Tag"; interface IProps { tags: string[]; @@ -80,10 +81,13 @@ export default class TagComposer extends React.PureComponent {
- { this.props.tags.map((t, i) => (
- { t } - -
)) } + { this.props.tags.map((t, i) => ( + + )) }
; } From 6b13988eaa959568d05e8bc63238b98fa19d1953 Mon Sep 17 00:00:00 2001 From: Yaya Usman <38439166+yaya-usman@users.noreply.github.com> Date: Tue, 19 Apr 2022 12:20:56 +0300 Subject: [PATCH 010/102] Fix: "Code formatting button does not escape backticks" (#8181) Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/editor/deserialize.ts | 2 +- src/editor/operations.ts | 11 +++-- test/editor/operations-test.ts | 85 ++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index 1fbccf45fff..7d4f94cdc3d 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -32,7 +32,7 @@ function escape(text: string): string { // Finds the length of the longest backtick sequence in the given text, used for // escaping backticks in code blocks -function longestBacktickSequence(text: string): number { +export function longestBacktickSequence(text: string): number { let length = 0; let currentLength = 0; diff --git a/src/editor/operations.ts b/src/editor/operations.ts index 6129681815c..40a438cc562 100644 --- a/src/editor/operations.ts +++ b/src/editor/operations.ts @@ -17,6 +17,7 @@ limitations under the License. import Range from "./range"; import { Part, Type } from "./parts"; import { Formatting } from "../components/views/rooms/MessageComposerFormatBar"; +import { longestBacktickSequence } from './deserialize'; /** * Some common queries and transformations on the editor model @@ -181,12 +182,12 @@ export function formatRangeAsCode(range: Range): void { const hasBlockFormatting = (range.length > 0) && range.text.startsWith("```") - && range.text.endsWith("```"); + && range.text.endsWith("```") + && range.text.includes('\n'); const needsBlockFormatting = parts.some(p => p.type === Type.Newline); if (hasBlockFormatting) { - // Remove previously pushed backticks and new lines parts.shift(); parts.pop(); if (parts[0]?.text === "\n" && parts[parts.length - 1]?.text === "\n") { @@ -205,7 +206,10 @@ export function formatRangeAsCode(range: Range): void { parts.push(partCreator.newline()); } } else { - toggleInlineFormat(range, "`"); + const fenceLen = longestBacktickSequence(range.text); + const hasInlineFormatting = range.text.startsWith("`") && range.text.endsWith("`"); + //if it's already formatted untoggle based on fenceLen which returns the max. num of backtick within a text else increase the fence backticks with a factor of 1. + toggleInlineFormat(range, "`".repeat(hasInlineFormatting ? fenceLen : fenceLen + 1)); return; } @@ -240,6 +244,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix // compute paragraph [start, end] indexes const paragraphIndexes = []; let startIndex = 0; + // start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end for (let i = 2; i < parts.length; i++) { // paragraph breaks can be denoted in a multitude of ways, diff --git a/test/editor/operations-test.ts b/test/editor/operations-test.ts index b9ab4cc4e85..3e4de224179 100644 --- a/test/editor/operations-test.ts +++ b/test/editor/operations-test.ts @@ -20,8 +20,10 @@ import { toggleInlineFormat, selectRangeOfWordAtCaret, formatRange, + formatRangeAsCode, } from "../../src/editor/operations"; import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar"; +import { longestBacktickSequence } from '../../src/editor/deserialize'; const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" }; @@ -43,6 +45,89 @@ describe('editor/operations: formatting operations', () => { expect(model.serializeParts()).toEqual([{ "text": "hello _world_!", "type": "plain" }]); }); + describe('escape backticks', () => { + it('works for escaping backticks in between texts', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("hello ` world!"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.positionForOffset(13, false)); // hello ` world + + expect(range.parts[0].text.trim().includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text.trim())).toBe(1); + expect(model.serializeParts()).toEqual([{ "text": "hello ` world!", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "``hello ` world``!", "type": "plain" }]); + }); + + it('escapes longer backticks in between text', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("hello```world"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hello```world + + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(3); + expect(model.serializeParts()).toEqual([{ "text": "hello```world", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "````hello```world````", "type": "plain" }]); + }); + + it('escapes non-consecutive with varying length backticks in between text', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("hell```o`w`o``rld"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hell```o`w`o``rld + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(3); + expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]); + }); + + it('untoggles correctly if its already formatted', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("```hello``world```"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hello``world + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(3); + expect(model.serializeParts()).toEqual([{ "text": "```hello``world```", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "hello``world", "type": "plain" }]); + }); + it('untoggles correctly it contains varying length of backticks between text', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("````hell```o`w`o``rld````"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hell```o`w`o``rld + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(4); + expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]); + }); + }); + it('works for parts of words', () => { const renderer = createRenderer(); const pc = createPartCreator(); From f70186ea9bff5b83401ea689e03fc6ae189f6d0c Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 19 Apr 2022 13:35:39 +0200 Subject: [PATCH 011/102] Live location sharing: set map bounds to include all locations (#8324) * open a dialog with map centered around first beacon Signed-off-by: Kerry Archibald * test dialog opening from beacon body Signed-off-by: Kerry Archibald * test beaconmarker Signed-off-by: Kerry Archibald * add bounds to Map comp Signed-off-by: Kerry Archibald * add focusBeacon to beaconviewdialog, use bounds Signed-off-by: Kerry Archibald * lint Signed-off-by: Kerry Archibald * use membercolor on beacon view markers Signed-off-by: Kerry Archibald * add lnglatbounds to maplibre mock Signed-off-by: Kerry Archibald * update snapshots for expanded maplibre Map mock Signed-off-by: Kerry Archibald * test map bounds Signed-off-by: Kerry Archibald * tidy copy paste comment Signed-off-by: Kerry Archibald * add fallback when no more live locations Signed-off-by: Kerry Archibald * accurate signature for getBoundsCenter Signed-off-by: Kerry Archibald --- __mocks__/maplibre-gl.js | 4 +- .../views/beacon/_BeaconViewDialog.scss | 22 ++++++++ src/components/views/beacon/BeaconMarker.tsx | 1 + .../views/beacon/BeaconViewDialog.tsx | 56 ++++++++++++++----- src/components/views/location/Map.tsx | 29 +++++++++- src/components/views/messages/MBeaconBody.tsx | 3 +- src/i18n/strings/en_EN.json | 1 + .../views/beacon/BeaconViewDialog-test.tsx | 34 +++++++++++ .../__snapshots__/BeaconMarker-test.tsx.snap | 6 +- .../BeaconViewDialog-test.tsx.snap | 37 ++++++++++++ test/components/views/location/Map-test.tsx | 32 +++++++++++ .../LocationViewDialog-test.tsx.snap | 2 + .../__snapshots__/SmartMarker-test.tsx.snap | 2 + .../__snapshots__/ZoomButtons-test.tsx.snap | 1 + .../views/messages/MBeaconBody-test.tsx | 36 +++++++++++- .../__snapshots__/MLocationBody-test.tsx.snap | 1 + 16 files changed, 246 insertions(+), 21 deletions(-) create mode 100644 test/components/views/beacon/__snapshots__/BeaconViewDialog-test.tsx.snap diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js index 90a30968d9b..599cacde13d 100644 --- a/__mocks__/maplibre-gl.js +++ b/__mocks__/maplibre-gl.js @@ -1,5 +1,5 @@ const EventEmitter = require("events"); -const { LngLat, NavigationControl } = require('maplibre-gl'); +const { LngLat, NavigationControl, LngLatBounds } = require('maplibre-gl'); class MockMap extends EventEmitter { addControl = jest.fn(); @@ -8,6 +8,7 @@ class MockMap extends EventEmitter { zoomOut = jest.fn(); setCenter = jest.fn(); setStyle = jest.fn(); + fitBounds = jest.fn(); } const MockMapInstance = new MockMap(); @@ -24,5 +25,6 @@ module.exports = { GeolocateControl: jest.fn().mockReturnValue(MockGeolocateInstance), Marker: jest.fn().mockReturnValue(MockMarker), LngLat, + LngLatBounds, NavigationControl, }; diff --git a/res/css/components/views/beacon/_BeaconViewDialog.scss b/res/css/components/views/beacon/_BeaconViewDialog.scss index 901b4564395..dc4d089bfe5 100644 --- a/res/css/components/views/beacon/_BeaconViewDialog.scss +++ b/res/css/components/views/beacon/_BeaconViewDialog.scss @@ -55,3 +55,25 @@ limitations under the License. height: 80vh; border-radius: 8px; } + +.mx_BeaconViewDialog_mapFallback { + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + background: url('$(res)/img/location/map.svg'); + background-size: cover; +} + +.mx_BeaconViewDialog_mapFallbackIcon { + width: 65px; + margin-bottom: $spacing-16; + color: $quaternary-content; +} + +.mx_BeaconViewDialog_mapFallbackMessage { + color: $secondary-content; + margin-bottom: $spacing-16; +} diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx index 8c176ab9c07..f7f284b88ed 100644 --- a/src/components/views/beacon/BeaconMarker.tsx +++ b/src/components/views/beacon/BeaconMarker.tsx @@ -58,6 +58,7 @@ const BeaconMarker: React.FC = ({ map, beacon }) => { id={beacon.identifier} geoUri={geoUri} roomMember={markerRoomMember} + useMemberColor />; }; diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 052a456fe69..12f26a0a54f 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -29,29 +29,43 @@ import { IDialogProps } from "../dialogs/IDialogProps"; import Map from '../location/Map'; import ZoomButtons from '../location/ZoomButtons'; import BeaconMarker from './BeaconMarker'; +import { Bounds, getBeaconBounds } from '../../../utils/beacon/bounds'; +import { getGeoUri } from '../../../utils/beacon'; +import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg'; +import { _t } from '../../../languageHandler'; +import AccessibleButton from '../elements/AccessibleButton'; interface IProps extends IDialogProps { roomId: Room['roomId']; matrixClient: MatrixClient; + // open the map centered on this beacon's location + focusBeacon?: Beacon; } -// TODO actual center is coming soon -// for now just center around first beacon in list -const getMapCenterUri = (beacons: Beacon[]): string => { - const firstBeaconWithLocation = beacons.find(beacon => beacon.latestLocationState); - - return firstBeaconWithLocation?.latestLocationState?.uri; +const getBoundsCenter = (bounds: Bounds): string | undefined => { + if (!bounds) { + return; + } + return getGeoUri({ + latitude: (bounds.north + bounds.south) / 2, + longitude: (bounds.east + bounds.west) / 2, + timestamp: Date.now(), + }); }; /** * Dialog to view live beacons maximised */ -const BeaconViewDialog: React.FC = ({ roomId, matrixClient, onFinished }) => { +const BeaconViewDialog: React.FC = ({ + focusBeacon, + roomId, + matrixClient, + onFinished, +}) => { const liveBeacons = useLiveBeacons(roomId, matrixClient); - const mapCenterUri = getMapCenterUri(liveBeacons); - // TODO probably show loader or placeholder when there is no location - // to center the map on + const bounds = getBeaconBounds(liveBeacons); + const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds); return ( = ({ roomId, matrixClient, onFinished } fixedWidth={false} > - @@ -77,7 +92,22 @@ const BeaconViewDialog: React.FC = ({ roomId, matrixClient, onFinished } } - + : +
+ + { _t('No live locations') } + + { _t('Close') } + +
+ }
); diff --git a/src/components/views/location/Map.tsx b/src/components/views/location/Map.tsx index 8776e8e8264..fc3bfab3eb7 100644 --- a/src/components/views/location/Map.tsx +++ b/src/components/views/location/Map.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { ReactNode, useContext, useEffect } from 'react'; import classNames from 'classnames'; +import maplibregl from 'maplibre-gl'; import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/matrix'; import { logger } from 'matrix-js-sdk/src/logger'; @@ -24,8 +25,9 @@ import { useEventEmitterState } from '../../../hooks/useEventEmitter'; import { parseGeoUri } from '../../../utils/location'; import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils'; import { useMap } from '../../../utils/location/useMap'; +import { Bounds } from '../../../utils/beacon/bounds'; -const useMapWithStyle = ({ id, centerGeoUri, onError, interactive }) => { +const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) => { const bodyId = `mx_Map_${id}`; // style config @@ -55,6 +57,20 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive }) => { } }, [map, centerGeoUri]); + useEffect(() => { + if (map && bounds) { + try { + const lngLatBounds = new maplibregl.LngLatBounds( + [bounds.west, bounds.south], + [bounds.east, bounds.north], + ); + map.fitBounds(lngLatBounds, { padding: 100 }); + } catch (error) { + logger.error('Invalid map bounds', error); + } + } + }, [map, bounds]); + return { map, bodyId, @@ -65,6 +81,7 @@ interface MapProps { id: string; interactive?: boolean; centerGeoUri?: string; + bounds?: Bounds; className?: string; onClick?: () => void; onError?: (error: Error) => void; @@ -74,9 +91,15 @@ interface MapProps { } const Map: React.FC = ({ - centerGeoUri, className, id, onError, onClick, children, interactive, + bounds, + centerGeoUri, + children, + className, + id, + interactive, + onError, onClick, }) => { - const { map, bodyId } = useMapWithStyle({ centerGeoUri, onError, id, interactive }); + const { map, bodyId } = useMapWithStyle({ centerGeoUri, onError, id, interactive, bounds }); const onMapClick = ( event: React.MouseEvent, diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index f61ec346e4f..4beac791019 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -32,8 +32,8 @@ import Spinner from '../elements/Spinner'; import Map from '../location/Map'; import SmartMarker from '../location/SmartMarker'; import OwnBeaconStatus from '../beacon/OwnBeaconStatus'; -import { IBodyProps } from "./IBodyProps"; import BeaconViewDialog from '../beacon/BeaconViewDialog'; +import { IBodyProps } from "./IBodyProps"; const useBeaconState = (beaconInfoEvent: MatrixEvent): { beacon?: Beacon; @@ -105,6 +105,7 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => { roomId: mxEvent.getRoomId(), matrixClient, + focusBeacon: beacon, }, "mx_BeaconViewDialog_wrapper", false, // isPriority diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index aacc7f577d9..46632c34499 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2915,6 +2915,7 @@ "Loading live location...": "Loading live location...", "Live location ended": "Live location ended", "Live location error": "Live location error", + "No live locations": "No live locations", "An error occured whilst sharing your live location": "An error occured whilst sharing your live location", "You are sharing your live location": "You are sharing your live location", "%(timeRemaining)s left": "%(timeRemaining)s left", diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx index 70dddd2710d..b3573de0f99 100644 --- a/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -26,6 +26,7 @@ import { import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog'; import { + findByTestId, getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent, @@ -118,4 +119,37 @@ describe('', () => { // two markers now! expect(component.find('BeaconMarker').length).toEqual(2); }); + + it('renders a fallback when no live beacons remain', () => { + const onFinished = jest.fn(); + const room = makeRoomWithStateEvents([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent({ onFinished }); + expect(component.find('BeaconMarker').length).toEqual(1); + + // this will replace the defaultEvent + // leading to no more live beacons + const anotherBeaconEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: false }, + '$bob-room1-1', + ); + + act(() => { + // emits RoomStateEvent.BeaconLiveness + room.currentState.setStateEvents([anotherBeaconEvent]); + }); + + component.setProps({}); + + // map placeholder + expect(findByTestId(component, 'beacon-view-dialog-map-fallback')).toMatchSnapshot(); + + act(() => { + findByTestId(component, 'beacon-view-dialog-fallback-close').at(0).simulate('click'); + }); + + expect(onFinished).toHaveBeenCalled(); + }); }); diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap index cde5fd8232c..e590cbcd9f3 100644 --- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap @@ -61,6 +61,7 @@ exports[` renders marker when beacon has location 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], @@ -79,6 +80,7 @@ exports[` renders marker when beacon has location 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], @@ -111,6 +113,7 @@ exports[` renders marker when beacon has location 1`] = ` Symbol(kCapture): false, } } + useMemberColor={true} > renders marker when beacon has location 1`] = ` Symbol(kCapture): false, } } + useMemberColor={true} >
renders a fallback when no live beacons remain 1`] = ` +
+
+ + No live locations + + +
+ Close +
+
+
+`; diff --git a/test/components/views/location/Map-test.tsx b/test/components/views/location/Map-test.tsx index 40aaee01a07..a1e1680b18a 100644 --- a/test/components/views/location/Map-test.tsx +++ b/test/components/views/location/Map-test.tsx @@ -115,6 +115,38 @@ describe('', () => { }); }); + describe('map bounds', () => { + it('does not try to fit map bounds when no bounds provided', () => { + getComponent({ bounds: null }); + expect(mockMap.fitBounds).not.toHaveBeenCalled(); + }); + + it('fits map to bounds', () => { + const bounds = { north: 51, south: 50, east: 42, west: 41 }; + getComponent({ bounds }); + expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds([bounds.west, bounds.south], + [bounds.east, bounds.north]), { padding: 100 }); + }); + + it('handles invalid bounds', () => { + const logSpy = jest.spyOn(logger, 'error').mockImplementation(); + const bounds = { north: 'a', south: 'b', east: 42, west: 41 }; + getComponent({ bounds }); + expect(mockMap.fitBounds).not.toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledWith('Invalid map bounds', new Error('Invalid LngLat object: (41, NaN)')); + }); + + it('updates map bounds when bounds prop changes', () => { + const component = getComponent({ centerGeoUri: 'geo:51,42' }); + + const bounds = { north: 51, south: 50, east: 42, west: 41 }; + const bounds2 = { north: 53, south: 51, east: 45, west: 44 }; + component.setProps({ bounds }); + component.setProps({ bounds: bounds2 }); + expect(mockMap.fitBounds).toHaveBeenCalledTimes(2); + }); + }); + describe('children', () => { it('renders without children', () => { const component = getComponent({ children: null }); diff --git a/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap b/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap index 41b4044c5aa..8a1910a5820 100644 --- a/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap +++ b/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap @@ -24,6 +24,7 @@ exports[` renders map correctly 1`] = ` "_eventsCount": 1, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction] { "calls": Array [ @@ -76,6 +77,7 @@ exports[` renders map correctly 1`] = ` "_eventsCount": 1, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction] { "calls": Array [ diff --git a/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap b/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap index b2da037e221..d20c9bcd6ce 100644 --- a/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap +++ b/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap @@ -9,6 +9,7 @@ exports[` creates a marker on mount 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], @@ -45,6 +46,7 @@ exports[` removes marker on unmount 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], diff --git a/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap b/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap index 7f18eccc82b..0fbc9851687 100644 --- a/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap +++ b/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap @@ -8,6 +8,7 @@ exports[` renders buttons 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], diff --git a/test/components/views/messages/MBeaconBody-test.tsx b/test/components/views/messages/MBeaconBody-test.tsx index 9ec5db5f2e1..b37bf65bbde 100644 --- a/test/components/views/messages/MBeaconBody-test.tsx +++ b/test/components/views/messages/MBeaconBody-test.tsx @@ -86,7 +86,6 @@ describe('', () => { }); const modalSpy = jest.spyOn(Modal, 'createTrackedDialog').mockReturnValue(undefined); - beforeEach(() => { jest.clearAllMocks(); }); @@ -123,7 +122,6 @@ describe('', () => { ); makeRoomWithStateEvents([beaconInfoEvent]); const component = getComponent({ mxEvent: beaconInfoEvent }); - act(() => { component.find('.mx_MBeaconBody_map').simulate('click'); }); @@ -268,6 +266,40 @@ describe('', () => { expect(modalSpy).toHaveBeenCalled(); }); + it('does nothing on click when a beacon has no location', () => { + makeRoomWithStateEvents([aliceBeaconInfo]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + act(() => { + component.find('.mx_MBeaconBody_map').simulate('click'); + }); + + expect(modalSpy).not.toHaveBeenCalled(); + }); + + it('renders a live beacon with a location correctly', () => { + const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); + beaconInstance.addLocations([location1]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + expect(component.find('Map').length).toBeTruthy; + }); + + it('opens maximised map view on click when beacon has a live location', () => { + const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); + beaconInstance.addLocations([location1]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + act(() => { + component.find('Map').simulate('click'); + }); + + // opens modal + expect(modalSpy).toHaveBeenCalled(); + }); + it('updates latest location', () => { const room = makeRoomWithStateEvents([aliceBeaconInfo]); const component = getComponent({ mxEvent: aliceBeaconInfo }); diff --git a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap index e32cedfde48..2edfc8e22d3 100644 --- a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap @@ -124,6 +124,7 @@ exports[`MLocationBody without error renders map correctly 1`] = "_eventsCount": 1, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction] { "calls": Array [ From 80c1fad0882866a5e7ac4a6951c3a8a86f1b216d Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Tue, 19 Apr 2022 11:39:45 +0000 Subject: [PATCH 012/102] RTL layout compatibility: bridges list on room settings page (#8243) --- .../dialogs/_RoomSettingsDialogBridges.scss | 157 ++++++++++-------- src/components/views/settings/BridgeTile.tsx | 18 +- 2 files changed, 95 insertions(+), 80 deletions(-) diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss index f18b4917cf1..07735ad0278 100644 --- a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss @@ -22,84 +22,99 @@ limitations under the License. margin: 0; padding: 0; } -} -.mx_RoomSettingsDialog_BridgeList li { - list-style-type: none; - padding: 5px; - margin-bottom: 8px; - border-width: 1px 1px; - border-color: $primary-hairline-color; - border-style: solid; - border-radius: 5px; + li { + list-style-type: none; - .column-icon { - float: left; - padding-right: 10px; + &.mx_RoomSettingsDialog_BridgeList_listItem { + display: flex; + flex-wrap: wrap; + gap: $spacing-8; + padding: 5px; + margin-bottom: $spacing-8; - * { + // border-style around each bridge list item + border-width: 1px 1px; + border-color: $primary-hairline-color; + border-style: solid; border-radius: 5px; - border: 1px solid $input-darker-bg-color; - } - - .noProtocolIcon { - width: 48px; - height: 48px; - background: $input-darker-bg-color; - border-radius: 5px; - } - .protocol-icon { - float: left; - margin-right: 5px; - img { - border-radius: 5px; - border-width: 1px 1px; - border-color: $primary-hairline-color; + .mx_RoomSettingsDialog_column_icon { + .mx_RoomSettingsDialog_protocolIcon, + .mx_RoomSettingsDialog_protocolIcon span, + .mx_RoomSettingsDialog_noProtocolIcon { + box-sizing: border-box; + border-radius: 5px; + border: 1px solid $input-darker-bg-color; + } + + .mx_RoomSettingsDialog_noProtocolIcon, + .mx_RoomSettingsDialog_protocolIcon img { + border-radius: 5px; + } + + .mx_RoomSettingsDialog_noProtocolIcon { + width: 48px; + height: 48px; + background: $input-darker-bg-color; + } + + .mx_RoomSettingsDialog_protocolIcon { + img { + border-width: 1px 1px; + border-color: $primary-hairline-color; + } + + span { + /* Correct letter placement */ + left: auto; + } + } } - span { - /* Correct letter placement */ - left: auto; - } - } - } - - .column-data { - display: inline-block; - width: 85%; - - > h3 { - margin-top: 0px; - margin-bottom: 0px; - font-size: 16pt; - color: $primary-content; - } - - > * { - margin-top: 4px; - margin-bottom: 0; - } - - .workspace-channel-details { - color: $primary-content; - font-weight: 600; - - .channel { - margin-left: 5px; - } - } - .metadata { - color: $muted-fg-color; - margin-bottom: 0; - overflow-y: visible; - text-overflow: ellipsis; - white-space: normal; - padding: 0; - - > li { - padding: 0; - border: 0; + .mx_RoomSettingsDialog_column_data { + display: inline-block; + width: 85%; + + .mx_RoomSettingsDialog_column_data_details, + .mx_RoomSettingsDialog_column_data_metadata, + .mx_RoomSettingsDialog_column_data_metadata li, + .mx_RoomSettingsDialog_column_data_protocolName { + margin-bottom: 0; + } + + .mx_RoomSettingsDialog_column_data_details, + .mx_RoomSettingsDialog_column_data_metadata { + margin-top: $spacing-4; + } + + .mx_RoomSettingsDialog_column_data_metadata li { + margin-top: $spacing-8; + } + + .mx_RoomSettingsDialog_column_data_protocolName { + margin-top: 0; + font-size: 16pt; + color: $primary-content; + } + + .mx_RoomSettingsDialog_workspace_channel_details { + color: $primary-content; + font-weight: $font-semi-bold; + + .mx_RoomSettingsDialog_channel { + margin-inline-start: 5px; + } + } + + .mx_RoomSettingsDialog_metadata { + color: $muted-fg-color; + margin-bottom: 0; + overflow-y: visible; + text-overflow: ellipsis; + white-space: normal; + padding: 0; + } } } } diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx index 7cc3d5e114a..860022c978a 100644 --- a/src/components/views/settings/BridgeTile.tsx +++ b/src/components/views/settings/BridgeTile.tsx @@ -115,7 +115,7 @@ export default class BridgeTile extends React.PureComponent { if (protocol.avatar_url) { const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64); - networkIcon = { url={avatarUrl} />; } else { - networkIcon =
; + networkIcon =
; } let networkItem = null; if (network) { @@ -146,19 +146,19 @@ export default class BridgeTile extends React.PureComponent { } const id = this.props.ev.getId(); - return (
  • -
    + return (
  • +
    { networkIcon }
    -
    -

    { protocolName }

    -

    +

    +

    { protocolName }

    +

    { networkItem } - { _t("Channel: ", {}, { + { _t("Channel: ", {}, { channelLink: () => channelLink, }) }

    -
      +
        { creator } { bot }
    From 68c5a55eedb2b0752aa0d702678fd9636cae7169 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 19 Apr 2022 14:44:15 +0200 Subject: [PATCH 013/102] use fake timers and restore mocks in MatrixClientPeg test (#8356) Signed-off-by: Kerry Archibald --- test/MatrixClientPeg-test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index f3f6826c15c..13a298d5a56 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -17,10 +17,12 @@ limitations under the License. import { advanceDateAndTime, stubClient } from "./test-utils"; import { MatrixClientPeg as peg } from "../src/MatrixClientPeg"; +jest.useFakeTimers(); + describe("MatrixClientPeg", () => { afterEach(() => { localStorage.clear(); - advanceDateAndTime(0); + jest.restoreAllMocks(); }); it("setJustRegisteredUserId", () => { @@ -32,7 +34,7 @@ describe("MatrixClientPeg", () => { expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(true); expect(peg.userRegisteredWithinLastHours(24)).toBe(true); - advanceDateAndTime(1 * 60 * 60 * 1000); + advanceDateAndTime(1 * 60 * 60 * 1000 + 1); expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(false); expect(peg.userRegisteredWithinLastHours(24)).toBe(true); @@ -50,7 +52,7 @@ describe("MatrixClientPeg", () => { expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(false); expect(peg.userRegisteredWithinLastHours(24)).toBe(false); - advanceDateAndTime(1 * 60 * 60 * 1000); + advanceDateAndTime(1 * 60 * 60 * 1000 + 1); expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(false); expect(peg.userRegisteredWithinLastHours(24)).toBe(false); From f4d935d88d08720cb085cfba6f1dab0af890eb19 Mon Sep 17 00:00:00 2001 From: Element Translate Bot Date: Tue, 19 Apr 2022 15:05:12 +0200 Subject: [PATCH 014/102] Translations update from Weblate (#8361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Greek) Currently translated at 99.4% (3348 of 3366 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/el/ * Translated using Weblate (Greek) Currently translated at 99.4% (3348 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/el/ * Translated using Weblate (Icelandic) Currently translated at 87.2% (2938 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Albanian) Currently translated at 99.8% (3361 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ * Translated using Weblate (Greek) Currently translated at 99.7% (3357 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/el/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Czech) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Galician) Currently translated at 99.0% (3334 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Icelandic) Currently translated at 87.4% (2943 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Czech) Currently translated at 100.0% (3369 of 3369 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Albanian) Currently translated at 99.8% (3363 of 3369 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ * Translated using Weblate (Icelandic) Currently translated at 88.7% (2990 of 3369 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3369 of 3369 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Czech) Currently translated at 100.0% (3370 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Italian) Currently translated at 100.0% (3370 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Icelandic) Currently translated at 89.6% (3021 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3370 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Greek) Currently translated at 100.0% (3370 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/el/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3370 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Finnish) Currently translated at 86.0% (2900 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fi/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3370 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Icelandic) Currently translated at 90.0% (3036 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Galician) Currently translated at 99.1% (3342 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Icelandic) Currently translated at 90.3% (3044 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Icelandic) Currently translated at 90.6% (3056 of 3370 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Hebrew) Currently translated at 73.2% (2466 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ * Translated using Weblate (Hebrew) Currently translated at 73.2% (2466 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ * Translated using Weblate (Hebrew) Currently translated at 73.2% (2467 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ * Translated using Weblate (Hebrew) Currently translated at 73.2% (2467 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ * Translated using Weblate (Hebrew) Currently translated at 73.3% (2469 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ * Translated using Weblate (Hebrew) Currently translated at 73.3% (2469 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ * Translated using Weblate (Swedish) Currently translated at 97.2% (3273 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Czech) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Albanian) Currently translated at 99.8% (3361 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ * Translated using Weblate (Norwegian Nynorsk) Currently translated at 31.5% (1061 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nn/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Italian) Currently translated at 100.0% (3367 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Swedish) Currently translated at 97.4% (3281 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (Polish) Currently translated at 61.5% (2072 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/ * Translated using Weblate (Polish) Currently translated at 61.5% (2072 of 3367 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/ * Translated using Weblate (Indonesian) Currently translated at 99.9% (3374 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Czech) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Galician) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Spanish) Currently translated at 99.5% (3363 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ * Translated using Weblate (Albanian) Currently translated at 99.8% (3368 of 3374 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ * Translated using Weblate (Czech) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Dutch) Currently translated at 94.9% (3206 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Swedish) Currently translated at 97.5% (3293 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (French) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Dutch) Currently translated at 99.2% (3350 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Dutch) Currently translated at 99.2% (3352 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Galician) Currently translated at 100.0% (3376 of 3376 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Czech) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Greek) Currently translated at 99.4% (3359 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/el/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Galician) Currently translated at 100.0% (3377 of 3377 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Czech) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Finnish) Currently translated at 87.9% (2974 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fi/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Galician) Currently translated at 100.0% (3380 of 3380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Czech) Currently translated at 100.0% (3387 of 3387 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Ukrainian) Currently translated at 99.8% (3381 of 3387 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3387 of 3387 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Ukrainian) Currently translated at 99.9% (3388 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3390 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (French) Currently translated at 99.5% (3374 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (French) Currently translated at 99.7% (3381 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3390 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3390 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Czech) Currently translated at 100.0% (3390 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3390 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3390 of 3390 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3392 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3392 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3392 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (French) Currently translated at 100.0% (3392 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Czech) Currently translated at 100.0% (3392 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3392 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Japanese) Currently translated at 94.4% (3203 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 95.0% (3223 of 3392 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Spanish) Currently translated at 99.6% (3380 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ * Translated using Weblate (Dutch) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Swedish) Currently translated at 97.1% (3296 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Czech) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Galician) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Ukrainian) Currently translated at 99.8% (3388 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Czech) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3393 of 3393 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3394 of 3394 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ Co-authored-by: MedeaGRNET Co-authored-by: Weblate Co-authored-by: Sveinn í Felli Co-authored-by: Besnik Bleta Co-authored-by: Jeff Huang Co-authored-by: Linerly Co-authored-by: waclaw66 Co-authored-by: Jozef Gaal Co-authored-by: Xose M Co-authored-by: Priit Jõerüüt Co-authored-by: random Co-authored-by: Theo Co-authored-by: Tuomas Hietala Co-authored-by: NetanelHarris Co-authored-by: SPiRiT Co-authored-by: LinAGKar Co-authored-by: Ihor Hordiichuk Co-authored-by: Nils Haugen Co-authored-by: UwUnyaa Co-authored-by: Piotr Strebski Co-authored-by: Denys Nykula Co-authored-by: iaiz Co-authored-by: Johan Smits Co-authored-by: Glandos Co-authored-by: Jiri Grönroos Co-authored-by: Lucas Co-authored-by: Suguru Hirahara --- src/i18n/strings/cs.json | 62 ++++++++-- src/i18n/strings/el.json | 10 +- src/i18n/strings/es.json | 71 ++++++++++- src/i18n/strings/et.json | 30 ++++- src/i18n/strings/fi.json | 85 ++++++++++++- src/i18n/strings/fr.json | 79 +++++++++++- src/i18n/strings/gl.json | 75 +++++++++++- src/i18n/strings/id.json | 50 ++++++-- src/i18n/strings/it.json | 14 ++- src/i18n/strings/ja.json | 77 ++++++++++-- src/i18n/strings/nl.json | 223 ++++++++++++++++++++++++++++++++-- src/i18n/strings/sk.json | 102 +++++++++++----- src/i18n/strings/sq.json | 14 ++- src/i18n/strings/sv.json | 26 +++- src/i18n/strings/uk.json | 45 ++++++- src/i18n/strings/zh_Hant.json | 45 ++++++- 16 files changed, 916 insertions(+), 92 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 18bf7ac891b..d9246337e8b 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -110,7 +110,7 @@ "Failed to reject invite": "Nepodařilo se odmítnout pozvánku", "Failed to send request.": "Odeslání žádosti se nezdařilo.", "Failed to set display name": "Nepodařilo se nastavit zobrazované jméno", - "Failed to unban": "Přijetí zpět se nezdařilo", + "Failed to unban": "Zrušení vykázání se nezdařilo", "Failed to upload profile picture!": "Nahrání profilového obrázku se nezdařilo!", "Failure to create room": "Vytvoření místnosti se nezdařilo", "Forget room": "Zapomenout místnost", @@ -208,7 +208,7 @@ "Unable to create widget.": "Nepodařilo se vytvořit widget.", "Unable to remove contact information": "Nepodařilo se smazat kontaktní údaje", "Unable to verify email address.": "Nepodařilo se ověřit e-mailovou adresu.", - "Unban": "Přijmout zpět", + "Unban": "Zrušit vykázání", "Unable to enable Notifications": "Nepodařilo se povolit oznámení", "Unmute": "Povolit", "Unnamed Room": "Nepojmenovaná místnost", @@ -408,10 +408,10 @@ "were banned %(count)s times|one": "byl(a) vykázán(a)", "was banned %(count)s times|other": "byli %(count)s krát vykázáni", "was banned %(count)s times|one": "byl(a) vykázán(a)", - "were unbanned %(count)s times|other": "byli %(count)s přijati zpět", - "were unbanned %(count)s times|one": "byl(a) přijat(a) zpět", - "was unbanned %(count)s times|other": "byli %(count)s krát přijati zpět", - "was unbanned %(count)s times|one": "byl(a) přijat(a) zpět", + "were unbanned %(count)s times|other": "měli %(count)s krát zrušeno vykázání", + "were unbanned %(count)s times|one": "měli zrušeno vykázání", + "was unbanned %(count)s times|other": "měl(a) %(count)s krát zrušeno vykázání", + "was unbanned %(count)s times|one": "má zrušeno vykázání", "were kicked %(count)s times|other": "byli %(count)s krát vyhozeni", "were kicked %(count)s times|one": "byli vyhozeni", "was kicked %(count)s times|other": "byl %(count)s krát vyhozen", @@ -1016,7 +1016,7 @@ "Name or Matrix ID": "Jméno nebo Matrix ID", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Varování: Upgrade místnosti automaticky převede všechny členy na novou verzi místnosti. Do staré místnosti pošleme odkaz na novou místnost - všichni členové na něj budou muset klepnout, aby se přidali do nové místnosti.", "Changes your avatar in this current room only": "Změní váš avatar jen v této místnosti", - "Unbans user with given ID": "Přijmout zpět uživatele s daným identifikátorem", + "Unbans user with given ID": "Zruší vykázání uživatele s daným identifikátorem", "Adds a custom widget by URL to the room": "Přidá do místnosti vlastní widget podle adresy URL", "Please supply a https:// or http:// widget URL": "Zadejte webovou adresu widgetu (začínající na https:// nebo http://)", "You cannot modify widgets in this room.": "V této místnosti nemůžete manipulovat s widgety.", @@ -2908,7 +2908,7 @@ "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s vykopl(a) uživatele %(targetName)s: %(reason)s", "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s zrušil(a) pozvání pro uživatele %(targetName)s", "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s zrušil(a) pozvání pro uživatele %(targetName)s: %(reason)s", - "%(senderName)s unbanned %(targetName)s": "%(senderName)s přijal(a) zpět uživatele %(targetName)s", + "%(senderName)s unbanned %(targetName)s": "%(senderName)s zrušil(a) vykázání uživatele %(targetName)s", "%(targetName)s left the room": "%(targetName)s opustil(a) místnost", "%(targetName)s left the room: %(reason)s": "%(targetName)s opustil(a) místnost: %(reason)s", "%(targetName)s rejected the invitation": "%(targetName)s odmítl(a) pozvání", @@ -3227,13 +3227,13 @@ "Shows all threads you’ve participated in": "Zobrazí všechna vlákna, kterých jste se zúčastnili", "My threads": "Moje vlákna", "They won't be able to access whatever you're not an admin of.": "Nebudou mít přístup ke všemu, čeho nejste správcem.", - "Unban them from specific things I'm able to": "Přijmout je zpět do konkrétních míst, do kterých jsem schopen", - "Unban them from everything I'm able to": "Přijmout je zpět všude, kam mohu", + "Unban them from specific things I'm able to": "Zrušit jejich vykázání z konkrétních míst, kde mám oprávnění", + "Unban them from everything I'm able to": "Zrušit jejich vykázání všude, kde mám oprávnění", "Ban them from specific things I'm able to": "Vykázat je z konkrétních míst, ze kterých jsem schopen", "Kick them from specific things I'm able to": "Vykopnout je z konkrétních míst, ze kterých jsem schopen", "Ban them from everything I'm able to": "Vykázat je všude, kde mohu", "Ban from %(roomName)s": "Vykázat z %(roomName)s", - "Unban from %(roomName)s": "Přijmout zpět do %(roomName)s", + "Unban from %(roomName)s": "Zrušit vykázání z %(roomName)s", "They'll still be able to access whatever you're not an admin of.": "Stále budou mít přístup ke všemu, čeho nejste správcem.", "Kick them from everything I'm able to": "Vykopnout je ze všeho, co to jde", "Kick from %(roomName)s": "Vykopnout z %(roomName)s", @@ -3830,5 +3830,43 @@ "%(count)s participants|other": "%(count)s účastníků", "New video room": "Nová video místnost", "New room": "Nová místnost", - "Video rooms (under active development)": "Video místnosti (v aktivním vývoji)" + "Video rooms (under active development)": "Video místnosti (v aktivním vývoji)", + "Give feedback": "Poskytnout zpětnou vazbu", + "%(featureName)s Beta feedback": "Zpětná vazba beta funkce %(featureName)s", + "Beta feature. Click to learn more.": "Beta funkce. Kliknutím získáte další informace.", + "Beta feature": "Beta funkce", + "Threads are a beta feature": "Vlákna jsou beta funkcí", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Při najetí na zprávu použijte možnost \"Odpovědět ve vlákně\".", + "Threads help keep your conversations on-topic and easy to track.": "Vlákna pomáhají udržovat konverzace k tématu a snadno je sledovat.", + "To leave, return to this page and use the “Leave the beta” button.": "Chcete-li odejít, vraťte se na tuto stránku a použijte tlačítko \"Opustit beta verzi\".", + "Use \"Reply in thread\" when hovering over a message.": "Po najetí na zprávu použijte možnost \"Odpovědět ve vlákně\".", + "How can I start a thread?": "Jak mohu založit vlákno?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Vlákna pomáhají udržovat konverzace k tématu a snadno je sledovat. Další informace.", + "Keep discussions organised with threads.": "Diskuse udržovat organizované pomocí vláken.", + "sends hearts": "posílá srdíčka", + "Sends the given message with hearts": "Odešle danou zprávu se srdíčky", + "Confirm signing out these devices|one": "Potvrďte odhlášení z tohoto zařízení", + "Confirm signing out these devices|other": "Potvrďte odhlášení z těchto zařízení", + "Live location ended": "Sdílení polohy živě skončilo", + "Loading live location...": "Načítání polohy živě...", + "View live location": "Zobrazit polohu živě", + "Live location enabled": "Poloha živě povolena", + "Live location error": "Chyba polohy živě", + "Live until %(expiryTime)s": "Živě do %(expiryTime)s", + "Yes, enable": "Ano, povolit", + "Do you want to enable threads anyway?": "Chcete i přesto vlákna povolit?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Váš domovský server v současné době nepodporuje vlákna, takže tato funkce může být nespolehlivá. Některé zprávy ve vláknech nemusí být spolehlivě dostupné. Dozvědět se více.", + "Partial Support for Threads": "Částečná podpora vláken", + "Ban from room": "Vykázat z místnosti", + "Unban from room": "Zrušit vykázání z místnosti", + "Ban from space": "Vykázat z prostoru", + "Unban from space": "Zrušit vykázání z prostoru", + "Jump to the given date in the timeline": "Přejít na zadané datum na časové ose", + "Right-click message context menu": "Klikněte pravým tlačítkem pro zobrazení kontextové nabídky", + "Remove from space": "Odebrat z prostoru", + "Disinvite from room": "Zrušit pozvánku do místnosti", + "Disinvite from space": "Zrušit pozvánku do prostoru", + "Tip: Use “%(replyInThread)s” when hovering over a message.": "Tip: Použijte \"%(replyInThread)s\" při najetí na zprávu.", + "To leave, return to this page and use the “%(leaveTheBeta)s” button.": "Chcete-li odejít, vraťte se na tuto stránku a použijte tlačítko \"%(leaveTheBeta)s\".", + "Use “%(replyInThread)s” when hovering over a message.": "Použijte \"%(replyInThread)s\" při najetí na zprávu." } diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index bcd0a8129fe..ee84cf5f12d 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -2129,7 +2129,7 @@ "Collapse reply thread": "Σύμπτυξη νήματος απάντησης", "Show preview": "Εμφάνιση προεπισκόπησης", "View source": "Προβολή πηγής", - "Forward": "Εμπρός", + "Forward": "Προώθηση", "Open in OpenStreetMap": "Άνοιγμα στο OpenStreetMap", "Not a valid Security Key": "Μη έγκυρο Κλειδί Ασφαλείας", "This looks like a valid Security Key!": "Αυτό φαίνεται να είναι ένα έγκυρο Κλειδί Ασφαλείας!", @@ -3443,5 +3443,11 @@ "An error occured whilst sharing your live location": "Παρουσιάστηκε σφάλμα κατά την κοινή χρήση της τρέχουσας τοποθεσίας σας", "Force complete": "Εξαναγκασμός ολοκλήρωσης", "Close dialog or context menu": "Κλείσιμο διαλόγου ή μενού περιβάλλοντος", - "Stop sharing and close": "Σταματήστε την κοινή χρήση και κλείστε" + "Stop sharing and close": "Σταματήστε την κοινή χρήση και κλείστε", + "Video rooms (under active development)": "Βίντεο δωμάτια (υπό ανάπτυξη)", + "To leave, return to this page and use the “Leave the beta” button.": "Για να αποχωρήσετε, επιστρέψτε σε αυτήν τη σελίδα και χρησιμοποιήστε το κουμπί \"Αποχώρηση από την έκδοση beta\".", + "Use \"Reply in thread\" when hovering over a message.": "Χρησιμοποιήστε την \"Απάντηση στο νήμα\" όταν τοποθετείτε το δείκτη του ποντικιού πάνω από ένα μήνυμα.", + "How can I start a thread?": "Πώς μπορώ να ξεκινήσω ένα νήμα;", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Τα νήματα βοηθούν στην καλύτερη οργάνωση των συζητήσεων και στην εύκολη παρακολούθηση. Μάθετε περισσότερα.", + "Keep discussions organised with threads.": "Διατηρήστε τις συζητήσεις οργανωμένες σε νήματα." } diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index d5d085d6abf..8f70a33ce06 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -19,7 +19,7 @@ "Change Password": "Cambiar la contraseña", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s cambió el nivel de acceso de %(powerLevelDiffText)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s cambió el nombre de la sala a %(roomName)s.", - "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s cambió el asunto a \"%(topic)s\".", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s cambió el asunto a «%(topic)s».", "Changes your display nickname": "Cambia tu apodo público", "Command error": "Error de comando", "Commands": "Comandos", @@ -3777,5 +3777,72 @@ "Send custom room account data event": "Enviar evento personalizado de cuenta de la sala", "Send custom timeline event": "Enviar evento personalizado de historial de mensajes", "If you can't find the room you're looking for, ask for an invite or create a new room.": "Si no encuentras la sala que buscas, pide que te inviten a ella o crea una nueva.", - "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "Ayúdanos a identificar problemas y a mejorar %(analyticsOwner)s. Comparte datos anónimos sobre cómo usas la aplicación para que entendamos mejor cómo usa la gente varios dispositivos. Generaremos un identificador aleatorio que usarán todos tus dispositivos." + "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "Ayúdanos a identificar problemas y a mejorar %(analyticsOwner)s. Comparte datos anónimos sobre cómo usas la aplicación para que entendamos mejor cómo usa la gente varios dispositivos. Generaremos un identificador aleatorio que usarán todos tus dispositivos.", + "Give feedback": "Danos tu opinión", + "Threads are a beta feature": "Los hilos son una funcionalidad beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Consejo: Usa «Responder en un hilo» al pasar el ratón sobre un mensaje.", + "Stop sharing and close": "Dejar de compartir y cerrar", + "Create room": "Crear sala", + "Create a video room": "Crear una sala de vídeo", + "Create video room": "Crear sala de vídeo", + "%(featureName)s Beta feedback": "Danos tu opinión sobre la beta de %(featureName)s", + "Beta feature. Click to learn more.": "Funcionalidad beta. Haz clic para más información.", + "Beta feature": "Funcionalidad beta", + "%(count)s participants|one": "1 participante", + "%(count)s participants|other": "%(count)s participantes", + "Try again later, or ask a room or space admin to check if you have access.": "Inténtalo más tarde, o pídele a alguien con permisos de administrador dentro de la sala o espacio que compruebe si tienes acceso.", + "This room or space is not accessible at this time.": "Esta sala o espacio no es accesible en este momento.", + "Are you sure you're at the right place?": "¿Seguro que estás en el sitio correcto?", + "This room or space does not exist.": "Esta sala o espacio no existe.", + "There's no preview, would you like to join?": "No hay previsualización. ¿Te quieres unir?", + "You can still join here.": "Todavía puedes unirte.", + "You were banned by %(memberName)s": "%(memberName)s te ha vetado", + "Forget this space": "Olvidar este espacio", + "You were removed by %(memberName)s": "%(memberName)s te ha sacado", + "Loading preview": "Cargando previsualización", + "Joining …": "Entrando…", + "New video room": "Nueva sala de vídeo", + "New room": "Nueva sala", + "View older version of %(spaceName)s.": "Ver versión antigua de %(spaceName)s.", + "Upgrade this space to the recommended room version": "Actualiza la versión de este espacio a la recomendada", + "Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Compartir ubicación en tiempo real: comparte tu ubicación actual (en desarrollo y, temporalmente, las ubicaciones persisten en el historial de la sala)", + "Video rooms (under active development)": "Salas de vídeo (actualmente en desarrollo)", + "To leave, return to this page and use the “Leave the beta” button.": "Para salir, vuelve a esta página y usa el botón «Salir de la beta».", + "Use \"Reply in thread\" when hovering over a message.": "Usa «Responder en hilo» mientras pasas el ratón sobre un mensaje.", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Los hilos ayudan a mantener las conversaciones centradas en un único tema y las hace fáciles de seguir. Más información.", + "How can I start a thread?": "¿Cómo puedo empezar un hilo?", + "The person who invited you has already left.": "La persona que te invitó ya no está aquí.", + "Sorry, your homeserver is too old to participate here.": "Lo siento, tu servidor base es demasiado antiguo. No puedes participar aquí.", + "There was an error joining.": "Ha ocurrido un error al entrar.", + "The user's homeserver does not support the version of the space.": "El servidor base del usuario no es compatible con la versión de este espacio.", + "User may or may not exist": "El usuario podría no existir", + "User does not exist": "El usuario no existe", + "User is already in the room": "El usuario ya está en la sala", + "User is already in the space": "El usuario ya está en el espacio", + "User is already invited to the room": "El usuario ya está invitado a la sala", + "User is already invited to the space": "El usuario ya está invitado al espacio", + "Threads help keep your conversations on-topic and easy to track.": "Los hilos ayudan a mantener tus conversaciones centradas y a que sean fáciles de seguir.", + "Live location enabled": "Ubicación en tiempo real activada", + "An error occured whilst sharing your live location": "Ha ocurrido un error al compartir tu ubicación en tiempo real", + "Live location error": "Error en la ubicación en tiempo real", + "Live location ended": "La ubicación en tiempo real ha terminado", + "Loading live location...": "Cargando ubicación en tiempo real…", + "Live until %(expiryTime)s": "En directo hasta %(expiryTime)s", + "View live location": "Ver ubicación en tiempo real", + "Ban from room": "Vetar de la sala", + "Unban from room": "Dejar de vetar de la sala", + "Ban from space": "Vetar del espacio", + "Unban from space": "Dejar de vetar del espacio", + "Disinvite from room": "Retirar la invitación a la sala", + "Remove from space": "Quitar del espacio", + "Disinvite from space": "Retirar la invitación al espacio", + "Confirm signing out these devices|other": "Confirma el cierre de sesión en estos dispositivos", + "Sends the given message with hearts": "Envía corazones junto al mensaje", + "sends hearts": "envía corazones", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Tu servidor base no es compatible con los hilos en este momento, por lo que podrían funcionar de forma inestable. Algunos mensajes en los hilos podrían no estar disponibles de forma estable. Más información.", + "Yes, enable": "Sí, activar", + "Do you want to enable threads anyway?": "¿Quieres activar los hilos de todos modos?", + "Partial Support for Threads": "Compatibilidad parcial con los hilos", + "Failed to join": "No ha sido posible unirse", + "You do not have permission to invite people to this space.": "No tienes permiso para invitar gente a este espacio." } diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index fc9a5cf0fb5..52ee41160c1 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -364,7 +364,7 @@ "System Alerts": "Süsteemi teated", "This room": "See jututuba", "Joining room …": "Liitun jututoaga …", - "Loading …": "Laadin …", + "Loading …": "Laadime…", "e.g. %(exampleValue)s": "näiteks %(exampleValue)s", "Could not find user in room": "Jututoast ei leidnud kasutajat", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Näita ajatempleid 12-tunnises vormingus (näiteks 2:30pl)", @@ -3838,5 +3838,31 @@ "Use \"Reply in thread\" when hovering over a message.": "Sõnumi kohal avanevast valikust kasuta „Vasta jutulõngas“ võimalust.", "How can I start a thread?": "Kuidas ma alustan jutulõnga?", "Threads help keep conversations on-topic and easy to track. Learn more.": "Jutulõngad aitavad hoida vestlusi teemakohastena ja jälgitavatena. Lisateavet leiad siit.", - "Keep discussions organised with threads.": "Halda vestlusi jutulõngadena." + "Keep discussions organised with threads.": "Halda vestlusi jutulõngadena.", + "sends hearts": "saadame südameid", + "Sends the given message with hearts": "Lisab sellele sõnumile südamed", + "Live location ended": "Reaalajas asukoha jagamine on lõppenud", + "Loading live location...": "Reaalajas asukoha laadmine...", + "View live location": "Vaata asukohta reaalajas", + "Confirm signing out these devices|one": "Kinnita selle seadme väljalogimine", + "Confirm signing out these devices|other": "Kinnita nende seadmete väljalogimine", + "Live location enabled": "Reaalajas asukoha jagamine on kasutusel", + "Live location error": "Viga asukoha jagamisel reaalajas", + "Live until %(expiryTime)s": "Kuvamine toimib kuni %(expiryTime)s", + "Ban from room": "Määra suhtluskeeld jututoas", + "Unban from room": "Eemalda suhtluskeeld jututoas", + "Ban from space": "Määra suhtluskeeld kogukonnas", + "Unban from space": "Eemalda suhtluskeeld kogukonnas", + "Yes, enable": "Jah, võta kasutusele", + "Do you want to enable threads anyway?": "Kas sa ikkagi soovid jutulõngad kasutusele võtta?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Sinu koduserver hetkel ei toeta jutulõngasid ning seega antud funktsionaalsus ei pruugi toimida korralikult. Kõik sõnumid jutulõngas ilmselt ei ole loetavad. Lisateave.", + "Partial Support for Threads": "Osaline jutulõngade tugi", + "Right-click message context menu": "Parema hiireklõpsuga ava sõnumi kontekstimenüü", + "Jump to the given date in the timeline": "Vaata ajajoont alates sellest kuupäevast", + "Remove from space": "Eemalda sellest kogukonnast", + "Disinvite from room": "Eemalda kutse jututuppa", + "Disinvite from space": "Eemalda kutse kogukonda", + "Tip: Use “%(replyInThread)s” when hovering over a message.": "Soovitus: Sõnumi kohal avanevast valikust kasuta „%(replyInThread)s“ võimalust.", + "To leave, return to this page and use the “%(leaveTheBeta)s” button.": "Lahkumiseks ava sama vaade ning klõpsi nuppu „%(leaveTheBeta)s“.", + "Use “%(replyInThread)s” when hovering over a message.": "Sõnumi kohal avanevast valikust kasuta „%(replyInThread)s“ võimalust." } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 2ebbfc1d0dc..7dce75a7bda 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -3269,5 +3269,88 @@ "User is already in the room": "Käyttäjä on jo huoneessa", "User is already invited to the room": "Käyttäjä on jo kutsuttu huoneeseen", "%(space1Name)s and %(space2Name)s": "%(space1Name)s ja %(space2Name)s", - "Failed to invite users to %(roomName)s": "Käyttäjien kutsuminen huoneeseen %(roomName)s epäonnistui" + "Failed to invite users to %(roomName)s": "Käyttäjien kutsuminen huoneeseen %(roomName)s epäonnistui", + "Open user settings": "Avaa käyttäjäasetukset", + "Redo edit": "Tee uudelleen muokkaus", + "Undo edit": "Kumoa muokkaus", + "Jump to last message": "Siirry viimeiseen viestiin", + "Jump to first message": "Siirry ensimmäiseen viestiin", + "Accessibility": "Saavutettavuus", + "Toggle webcam on/off": "Kamera päälle/pois", + "[number]": "[numero]", + "Your new device is now verified. Other users will see it as trusted.": "Uusi laitteesi on nyt vahvistettu. Muut käyttäjät näkevät sen luotettuna.", + "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uusi laitteesi on nyt vahvistettu. Laitteella on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna.", + "Verify with another device": "Vahvista toisella laitteella", + "The email address doesn't appear to be valid.": "Sähköpostiosoite ei vaikuta kelvolliselta.", + "Device verified": "Laite vahvistettu", + "Verify this device": "Vahvista tämä laite", + "Unable to verify this device": "Tätä laitetta ei voitu vahvistaa", + "Do not disturb": "Älä häiritse", + "Set a new status": "Aseta uusi tila", + "Give feedback": "Anna palautetta", + "Failed to load list of rooms.": "Huoneluettelon lataaminen epäonnistui.", + "Joined": "Liitytty", + "Joining": "Liitytään", + "Wait!": "Odota!", + "Own your conversations.": "Omista keskustelusi.", + "Unnamed audio": "Nimetön ääni", + "Stop sharing and close": "Lopeta jakaminen ja sulje", + "Stop sharing": "Lopeta jakaminen", + "%(timeRemaining)s left": "%(timeRemaining)s jäljellä", + "Click for more info": "Napsauta tästä saadaksesi lisätietoja", + "This is a beta feature": "Tämä on beetaominaisuus", + "Start audio stream": "Käynnistä äänen suoratoisto", + "Unable to start audio streaming.": "Äänen suoratoiston aloittaminen ei onnistu.", + "Open in OpenStreetMap": "Avaa OpenStreetMapissa", + "No verification requests found": "Vahvistuspyyntöjä ei löytynyt", + "Observe only": "Tarkkaile ainoastaan", + "Requester": "Pyytäjä", + "Methods": "Menetelmät", + "Timeout": "Aikakatkaisu", + "Phase": "Vaihe", + "Transaction": "Transaktio", + "Cancelled": "Peruttu", + "Started": "Käynnistetty", + "Ready": "Valmis", + "Requested": "Pyydetty", + "Edit setting": "Muokkaa asetusta", + "Edit values": "Muokkaa arvoja", + "Failed to save settings.": "Asetusten tallentaminen epäonnistui.", + "Number of users": "Käyttäjämäärä", + "Server": "Palvelin", + "Failed to load.": "Lataaminen epäonnistui.", + "Capabilities": "Kyvykkyydet", + "Doesn't look like valid JSON.": "Ei vaikuta kelvolliselta JSON:ilta.", + "Verify other device": "Vahvista toinen laite", + "Use to scroll": "Käytä vierittääksesi", + "Clear": "Tyhjennä", + "Join %(roomAddress)s": "Liity %(roomAddress)s", + "Link to room": "Linkitä huoneeseen", + "Matrix.org is the biggest public homeserver in the world, so it's a good place for many.": "Matrix.org on maailman suurin julkinen kotipalvelin, joten se on hyvä valinta useimmille.", + "Automatically invite members from this room to the new one": "Kutsu jäsenet tästä huoneesta automaattisesti uuteen huoneeseen", + "Spam or propaganda": "Roskapostitusta tai propagandaa", + "Toxic Behaviour": "Myrkyllinen käyttäytyminen", + "Feedback sent! Thanks, we appreciate it!": "Palaute lähetetty. Kiitos, arvostamme sitä!", + "Server info": "Palvelimen tiedot", + "Create room": "Luo huone", + "Create video room": "Luo videohuone", + "Create a video room": "Luo videohuone", + "%(featureName)s Beta feedback": "Ominaisuuden %(featureName)s beetapalaute", + "%(count)s members including you, %(commaSeparatedMembers)s|one": "%(count)s jäsentä mukaan lukien sinä ja %(commaSeparatedMembers)s", + "%(count)s members including you, %(commaSeparatedMembers)s|other": "%(count)s jäsentä mukaan lukien sinä, %(commaSeparatedMembers)s", + "Including you, %(commaSeparatedMembers)s": "Mukaan lukien sinä, %(commaSeparatedMembers)s", + "Beta feature. Click to learn more.": "Beetaominaisuus. Napsauta saadaksesi lisätietoja.", + "Beta feature": "Beetaominaisuus", + "The beginning of the room": "Huoneen alku", + "Last month": "Viime kuukausi", + "Last week": "Viime viikko", + "%(count)s participants|one": "1 osallistuja", + "%(count)s participants|other": "%(count)s osallistujaa", + "Connected": "Yhdistetty", + "Copy room link": "Kopioi huoneen linkki", + "New video room": "Uusi videohuone", + "New room": "Uusi huone", + "The new search": "Uusi haku", + "New search experience": "Uusi hakukokemus", + "That link is no longer supported": "Se linkki ei ole enää tuettu" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 9613c106134..7f359f98a04 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3778,5 +3778,82 @@ "%(value)sh": "%(value)sh", "%(value)sd": "%(value)sd", "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Si la page contient des informations permettant de vous identifier, comme un salon ou un identifiant d’utilisateur, ces données sont enlevées avant qu’elle ne soit envoyée au serveur.", - "If you can't find the room you're looking for, ask for an invite or create a new room.": "Si vous ne trouvez pas le salon que vous cherchez, demandez une invitation ou créez un nouveau salon." + "If you can't find the room you're looking for, ask for an invite or create a new room.": "Si vous ne trouvez pas le salon que vous cherchez, demandez une invitation ou créez un nouveau salon.", + "Give feedback": "Faire un commentaire", + "Threads are a beta feature": "Les fils de discussion sont une fonctionnalité bêta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Conseil : Utilisez « Répondre dans le fil de discussion » en survolant un message.", + "Threads help keep your conversations on-topic and easy to track.": "Les fils de discussion vous permettent de recentrer vos conversations et de les rendre facile à suivre.", + "Stop sharing and close": "Arrêter le partage et fermer", + "An error occurred while stopping your live location, please try again": "Une erreur s’est produite en arrêtant le partage de votre position, veuillez réessayer", + "An error occured whilst sharing your live location, please try again": "Une erreur s’est produite pendant le partage de votre position, veuillez réessayer plus tard", + "An error occured whilst sharing your live location": "Une erreur s’est produite pendant le partage de votre position", + "Create room": "Créer un salon", + "Create video room": "Crée le salon visio", + "Create a video room": "Créer un salon visio", + "%(featureName)s Beta feedback": "Commentaires sur la bêta de %(featureName)s", + "Beta feature. Click to learn more.": "Fonctionnalité bêta. Cliquez pour en savoir plus.", + "Beta feature": "Fonctionnalité bêta", + "%(count)s participants|one": "1 participant", + "%(count)s participants|other": "%(count)s participants", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s a été retourné en essayant d’accéder au salon. Si vous pensez que vous ne devriez pas voir ce message, veuillez soumettre un rapport d’anomalie.", + "Try again later, or ask a room or space admin to check if you have access.": "Réessayez plus tard ou demandez à l’administrateur du salon ou de l’espace si vous y avez accès.", + "This room or space is not accessible at this time.": "Ce salon ou cet espace n’est pas accessible en ce moment.", + "Are you sure you're at the right place?": "Êtes-vous sûr d’être au bon endroit ?", + "This room or space does not exist.": "Ce salon ou cet espace n’existe pas.", + "There's no preview, would you like to join?": "Il n’y a pas d’aperçu, voulez-vous rejoindre ?", + "This invite was sent to %(email)s": "Cet invitation a été envoyée à %(email)s", + "This invite was sent to %(email)s which is not associated with your account": "Cette invitation a été envoyée à %(email)s qui n’est pas associé à votre compte", + "You can still join here.": "Vous pouvez toujours rejoindre cet endroit.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "Une erreur (%(errcode)s) s’est produite en essayant de valider votre invitation. Vous pouvez essayer de transmettre cette information à la personne qui vous a invité(e).", + "Something went wrong with your invite.": "Quelque chose s’est mal passé avec votre invitation.", + "You were banned by %(memberName)s": "Vous avez été banni par %(memberName)s", + "Forget this space": "Oublier cet espace", + "You were removed by %(memberName)s": "Vous avez été retiré par %(memberName)s", + "Loading preview": "Chargement de l’aperçu", + "Joining …": "En train de rejoindre…", + "New video room": "Nouveau salon visio", + "New room": "Nouveau salon", + "View older version of %(spaceName)s.": "Voir l’ancienne version de %(spaceName)s.", + "Upgrade this space to the recommended room version": "Mettre à niveau cet espace vers la version recommandée", + "sends hearts": "envoie des cœurs", + "Sends the given message with hearts": "Envoie le message donné avec des cœurs", + "Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Partage de position en direct – partage votre position actuelle (développement en cours, et temporairement, les positions sont persistantes dans l’historique du salon)", + "Video rooms (under active development)": "Salons visios (en développement actif)", + "To leave, return to this page and use the “Leave the beta” button.": "Pour quitter, revenez à cette page et utilisez le bouton « Quitter la bêta ».", + "Use \"Reply in thread\" when hovering over a message.": "Utilisez « Répondre dans le fil de discussion » en survolant un message.", + "How can I start a thread?": "Comment démarrer un fil de discussion ?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Les fils de discussion aident à recentrer les conversations et les rends faciles à suivre. En savoir plus.", + "Keep discussions organised with threads.": "Gardez vos conversations organisées avec les fils de discussion.", + "Failed to join": "Impossible de rejoindre", + "The person who invited you has already left, or their server is offline.": "La personne qui vous a invité(e) a déjà quitté le salon, ou son serveur est hors-ligne.", + "The person who invited you has already left.": "La personne qui vous a invité(e) a déjà quitté le salon.", + "Sorry, your homeserver is too old to participate here.": "Désolé, votre serveur d'accueil est trop vieux pour participer ici.", + "There was an error joining.": "Il y a eu une erreur en rejoignant.", + "The user's homeserver does not support the version of the space.": "Le serveur d’accueil de l’utilisateur ne prend pas en charge la version de cet espace.", + "User may or may not exist": "L’utilisateur existe peut-être", + "User does not exist": "L’utilisateur n’existe pas", + "User is already in the room": "L’utilisateur est déjà dans ce salon", + "User is already in the space": "L’utilisateur est déjà dans cet espace", + "User is already invited to the room": "L’utilisateur a déjà été invité dans ce salon", + "User is already invited to the space": "L’utilisateur a déjà été invité dans cet espace", + "You do not have permission to invite people to this space.": "Vous n’avez pas la permission d’inviter des personnes dans cet espace.", + "Failed to invite users to %(roomName)s": "Impossible d’inviter les utilisateurs dans %(roomName)s", + "Jump to the given date in the timeline": "Aller à la date correspondante dans la discussion", + "View live location": "Voir la position en direct", + "Ban from room": "Bannir du salon", + "Unban from room": "Révoquer le bannissement du salon", + "Ban from space": "Bannir de l'espace", + "Confirm signing out these devices|one": "Confirmer la déconnexion de cet appareil", + "Confirm signing out these devices|other": "Confirmer la déconnexion de ces appareils", + "Yes, enable": "Oui, activer", + "Do you want to enable threads anyway?": "Voulez-vous activer les fils de discussions malgré tout ?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Votre serveur d'accueil ne prend pas actuellement en charge les fils de discussions, cette fonctionnalité peut donc ne pas être fiable. Certains messages dans les fils de discussions peuvent ne pas être disponibles de manière fiable. En savoir plus.", + "Live location enabled": "Position en temps réel activée", + "Live location error": "Erreur de positionnement en temps réel", + "Live location ended": "Position en temps réel terminée", + "Loading live location...": "Chargement de la position en direct…", + "Live until %(expiryTime)s": "En direct jusqu’à %(expiryTime)s", + "Unban from space": "Révoquer le bannissement de l’espace", + "Partial Support for Threads": "Prise en charge partielle des fils de discussions", + "Right-click message context menu": "Menu contextuel du message avec clic-droit" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 332299eb656..63679ba2654 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3284,7 +3284,7 @@ "The homeserver the user you're verifying is connected to": "O servidor ao que está conectado a persoa que estás verificando", "Can't see what you're looking for?": "Non atopas o que buscas?", "You do not have permission to start polls in this room.": "Non tes permiso para publicar enquisas nesta sala.", - "Reply in thread": "Responder na conversa", + "Reply in thread": "Responder nun fío", "Manage rooms in this space": "Xestionar salas neste espazo", "You won't get any notifications": "Non recibirás ningunha notificación", "Get notifications as set up in your settings": "Ter notificacións tal como se indica nos axustes", @@ -3782,5 +3782,76 @@ "User is already invited to the room": "A usuaria xa está convidada á sala", "User is already invited to the space": "A usuaria xa está convidada ao espazo", "You do not have permission to invite people to this space.": "Non tes permiso para convidar persoas a este espazo.", - "Failed to invite users to %(roomName)s": "Fallou o convite das usuarias para %(roomName)s" + "Failed to invite users to %(roomName)s": "Fallou o convite das usuarias para %(roomName)s", + "Give feedback": "Informar e dar opinión", + "Threads are a beta feature": "Os fíos son unha ferramenta beta", + "Threads help keep your conversations on-topic and easy to track.": "Os fíos axúdanche a manter as conversas no tema e facilitan o seguimento.", + "Stop sharing and close": "Deter a compartición e pechar", + "An error occurred while stopping your live location, please try again": "Algo fallou ao deter a túa localización en directo, inténtao outra vez", + "An error occured whilst sharing your live location, please try again": "Algo fallou ao compartir a túa localización en directo, inténtao máis tarde", + "An error occured whilst sharing your live location": "Algo fallou ao compartir a túa localización en directo", + "Create room": "Crear sala", + "Create video room": "Crear sala de vídeo", + "Create a video room": "Crear sala de vídeo", + "%(featureName)s Beta feedback": "Informe sobre %(featureName)s Beta", + "Beta feature. Click to learn more.": "Característica beta. Preme para saber máis.", + "Beta feature": "Característica Beta", + "%(count)s participants|one": "1 participante", + "%(count)s participants|other": "%(count)s participantes", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "Obtívose o erro %(errcode)s ao intentar acceder á sala ou espazo. Se cres que esta mensaxe é un erro, por favor envía un informe do fallo.", + "Try again later, or ask a room or space admin to check if you have access.": "Inténtao máis tarde, ou solicita a admin da sala ou espazo que mire se tes acceso.", + "This room or space is not accessible at this time.": "Esta sala ou espazo non é accesible neste intre.", + "Are you sure you're at the right place?": "Tes a certeza de que é o lugar correcto?", + "This room or space does not exist.": "Esta sala ou espazo no existe.", + "There's no preview, would you like to join?": "Non hai vista previa, queres unirte?", + "This invite was sent to %(email)s": "Este convite enviouse a %(email)s", + "This invite was sent to %(email)s which is not associated with your account": "O convite enviouselle a %(email)s que non está asociado coa túa conta", + "You can still join here.": "Podes entrar aquí igualmente.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "Houbo un erro (%(errcode)s) ao intentar validar o teu convite. Podes intentar enviarlle esta información á persoa que te convidou.", + "Something went wrong with your invite.": "Algo foi mal co teu convite.", + "You were banned by %(memberName)s": "%(memberName)s vetoute", + "Forget this space": "Esquecer este espazo", + "You were removed by %(memberName)s": "%(memberName)s eliminoute de aquí", + "Loading preview": "Cargando vista previa", + "Joining …": "Entrando…", + "New video room": "Nova sala de vídeo", + "New room": "Nova sala", + "View older version of %(spaceName)s.": "Ver versión anterior de %(spaceName)s.", + "Upgrade this space to the recommended room version": "Actualiza este espazo á última versión recomendada da sala", + "Video rooms (under active development)": "Salas de vídeo (en desenvolvemento activo)", + "To leave, return to this page and use the “Leave the beta” button.": "Para saír, volve a esta páxina e usa o botón \"Saír da beta\".", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Truco: Us \"Responder nun fío\" ao situarte sobre unha mensaxe.", + "Use \"Reply in thread\" when hovering over a message.": "Usa \"Responder nun fío\" cando te sitúes nunha mensaxe.", + "How can I start a thread?": "Como abrir un fío?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Os fíos axudan a centrar a conversa nun tema e facilitan o seguimento. Coñece máis.", + "Keep discussions organised with threads.": "Marter as conversas organizadas en fíos.", + "Failed to join": "Non puideches entrar", + "The person who invited you has already left, or their server is offline.": "A persoa que te convidou xa saíu, ou o seu servidor non está conectado.", + "The person who invited you has already left.": "A persoa que te convidou xa deixou o lugar.", + "Sorry, your homeserver is too old to participate here.": "Lamentámolo, o teu servidor de inicio é demasiado antigo para poder participar.", + "There was an error joining.": "Houbo un erro ao unirte.", + "The user's homeserver does not support the version of the space.": "O servidor de inicio da usuaria non soporta a versión do Espazo.", + "sends hearts": "envía corazóns", + "Sends the given message with hearts": "Engádelle moitos corazóns á mensaxe", + "Confirm signing out these devices|one": "Confirma a desconexión deste dispositivo", + "Confirm signing out these devices|other": "Confirma a desconexión destos dispositivos", + "Live location ended": "Rematou a localización en directo", + "Loading live location...": "Obtendo localización en directo...", + "View live location": "Ver localización en directo", + "Live location enabled": "Activada a localización en directo", + "Live location error": "Erro na localización en directo", + "Live until %(expiryTime)s": "En directo ata %(expiryTime)s", + "Ban from room": "Vetar na sala", + "Unban from room": "Retirar veto á sala", + "Ban from space": "Vetar ao espazo", + "Unban from space": "Retirar veto ao espazo", + "Disinvite from room": "Retirar convite á sala", + "Remove from space": "Retirar do espazo", + "Disinvite from space": "Retirar convite ao espazo", + "Yes, enable": "Si, activádeos", + "Do you want to enable threads anyway?": "Queres activar os fíos igualmente?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "O teu servidor actualmente non ten soporte para fíos, polo que podería non ser totalmente fiable. Algún dos comentarios fiados poderían non estar dispoñibles. Saber máis.", + "Partial Support for Threads": "Soporte parcial para Fíos", + "Right-click message context menu": "Botón dereito para menú contextual", + "Jump to the given date in the timeline": "Ir á seguinte data dada na cronoloxía" } diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 420ad1e9062..3038fd5ecc6 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -1641,7 +1641,7 @@ "Support adding custom themes": "Dukungan penambahan tema kustom", "Try out new ways to ignore people (experimental)": "Coba cara yang baru untuk mengabaikan pengguna (eksperimental)", "Multiple integration managers (requires manual setup)": "Beberapa manajer integrasi (membutuhkan penyiapan manual)", - "Render simple counters in room header": "Tampilkan penghitung simpel di tajukan ruangan", + "Render simple counters in room header": "Tampilkan penghitung sederhana di tajukan ruangan", "Group & filter rooms by custom tags (refresh to apply changes)": "Kelompokkan & filter ruangan dengan tag kustom (muat ulang untuk menerapkan perubahan)", "Custom user status messages": "Pesan status pengguna kustom", "Threaded messaging": "Pesan utasan", @@ -1693,7 +1693,7 @@ "Don't miss a reply": "Jangan lewatkan sebuah balasan", "Review to ensure your account is safe": "Periksa untuk memastikan akun Anda aman", "You have unverified logins": "Anda punya login yang belum diverifikasi", - "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Mengirimkan data penggunaan anonim yang akan bantu kami untuk membuat %(brand)s lebih baik. Ini akan menggunakan sebuah cookie.", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Mengirimkan data penggunaan anonim yang akan bantu kami untuk membuat %(brand)s lebih baik. Ini akan menggunakan sebuah kuki.", "Help us improve %(brand)s": "Bantu kami membuat %(brand)s lebih baik", "File Attached": "File Dilampirkan", "Error fetching file": "Terjadi kesalahan saat mendapatkan file", @@ -1751,7 +1751,7 @@ "Spaces are ways to group rooms and people.": "Space adalah salah satu cara untuk mengelompokkan ruangan dan pengguna.", "Sidebar": "Bilah Samping", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Kelola sesi Anda di bawah. Sebuah nama sesi dapat dilihat oleh siapa saja yang Anda berkomunikasi.", - "Where you're signed in": "Dimana Anda masuk", + "Where you're signed in": "Di mana Anda masuk", "Learn more about how we use analytics.": "Pelajari lebih lanjut tentang bagaimana kamu menggunakan analitik.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privasi itu penting bagi kami, jadi kami tidak mengumpulkan data personal atau data yang dapat diidentifikasi untuk analitik kami.", "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s mengumpulkan analitik anonim untuk membantu kami untuk membuat aplikasi ini lebih baik.", @@ -1863,7 +1863,7 @@ "Waiting for %(displayName)s to accept…": "Menunggu untuk %(displayName)s untuk menerima…", "To proceed, please accept the verification request on your other login.": "Untuk melanjutkan, mohon terima permintaan verifikasi di login Anda yang lain.", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Ketika seseorang menambahkan URL di pesannya, sebuah tampilan URL dapat ditampilkan untuk memberikan informasi lainnya tentang tautan itu seperti judul, deskripsi, dan sebuah gambar dari website.", - "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Di ruangan terenkripsi, seperti ruangan ini, tampilan URL dinonaktifkan untuk memastikan homeserver Anda (dimana tampilannya dibuat) tidak mendapatkan informasi tentang tautan yang Anda lihat di ruangan ini.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Di ruangan terenkripsi, seperti ruangan ini, tampilan URL dinonaktifkan untuk memastikan homeserver Anda (di mana tampilannya dibuat) tidak mendapatkan informasi tentang tautan yang Anda lihat di ruangan ini.", "URL previews are disabled by default for participants in this room.": "Tampilan URL dinonaktifkan secara default untuk anggota di ruangan ini.", "URL previews are enabled by default for participants in this room.": "Tampilan URL diaktifkan secara default untuk anggota di ruangan ini.", "You have disabled URL previews by default.": "Anda telah menonaktifkan tampilan URL secara default.", @@ -2554,7 +2554,7 @@ "Message search initialisation failed, check your settings for more information": "Initialisasi pencarian pesan gagal, periksa pengaturan Anda untuk informasi lanjut", "Error - Mixed content": "Terjadi kesalahan — Konten tercampur", "Error loading Widget": "Terjadi kesalahan saat memuat Widget", - "This widget may use cookies.": "Widget ini mungkin menggunakan cookie.", + "This widget may use cookies.": "Widget ini mungkin menggunakan kuki.", "Widget added by": "Widget ditambahkan oleh", "Widgets do not use message encryption.": "Widget tidak menggunakan enkripsi pesan.", "Using this widget may share data with %(widgetDomain)s.": "Menggunakan widget ini mungkin membagikan data dengan %(widgetDomain)s.", @@ -2631,7 +2631,7 @@ "You should know": "Anda seharusnya tahu", "Terms of Service": "Persyaratan Layanan", "Privacy Policy": "Kebijakan Privasi", - "Cookie Policy": "Kebijakan Cookie", + "Cookie Policy": "Kebijakan Kuki", "Learn more in our , and .": "Pelajari lebih lanjut di , , dan .", "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Melanjutkan untuk sementara mengizinkan proses pengaturan %(hostSignupBrand)s untuk mengakses akun Anda untuk mendapatkan alamat-alamat email yang terverifikasi. Data ini tidak disimpan.", "Failed to connect to your homeserver. Please close this dialog and try again.": "Gagal menghubungkan ke homeserver Anda. Mohon tutup dialog ini dan coba lagi.", @@ -3080,7 +3080,7 @@ "Verify with Security Key or Phrase": "Verifikasi dengan Kunci Keamanan atau Frasa", "Proceed with reset": "Lanjutkan dengan mengatur ulang", "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.": "Sepertinya Anda tidak memiliki Kunci Keamanan atau perangkat lainnya yang Anda dapat gunakan untuk memverifikasi. Perangkat ini tidak dapat mengakses ke pesan terenkripsi lama. Untuk membuktikan identitas Anda, kunci verifikasi harus diatur ulang.", - "Decide where your account is hosted": "Putuskan dimana untuk menghost akun Anda", + "Decide where your account is hosted": "Putuskan di mana untuk menghost akun Anda", "Host account on": "Host akun di", "You can now close this window or log in to your new account.": "Anda dapat menutup jendela ini atau masuk ke akun yang baru.", "Log in to your new account.": "Masuk ke akun yang baru.", @@ -3126,7 +3126,7 @@ "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Jika Anda mau, dicatat bahwa pesan-pesan Anda tidak dihapus, tetapi pengalaman pencarian mungkin terdegradasi untuk beberapa saat indeksnya sedang dibuat ulang", "You most likely do not want to reset your event index store": "Kemungkinan besar Anda tidak ingin mengatur ulang penyimpanan indeks peristiwa Anda", "Reset event store?": "Atur ulang penyimanan peristiwa?", - "About homeservers": "Tentang homeserver-homeserver", + "About homeservers": "Tentang homeserver", "Continuing without email": "Melanjutkan tanpa email", "Data on this screen is shared with %(widgetDomain)s": "Data di layar ini dibagikan dengan %(widgetDomain)s", "Modal Widget": "Widget Modal", @@ -3347,7 +3347,7 @@ "Help improve %(analyticsOwner)s": "Bantu membuat %(analyticsOwner)s lebih baik", "That's fine": "Saya tidak keberatan", "Some examples of the information being sent to us to help make %(brand)s better includes:": "Beberapa contoh informasi yang akan dikirim ke kami untuk membuat %(brand)s lebih baik termasuk:", - "Our complete cookie policy can be found here.": "Kebijakan cookie kami yang lengkap dapat ditemukan di sini.", + "Our complete cookie policy can be found here.": "Kebijakan kuki kami yang lengkap dapat ditemukan di sini.", "Type of location share": "Ketik deskripsi", "My location": "Lokasi saya", "Share my current location as a once off": "Bagikan lokasi saya saat ini (sekali saja)", @@ -3672,7 +3672,7 @@ "Connected": "Terhubung", "Voice & video rooms (under active development)": "Ruangan suara & video (dalam pengembangan aktif)", "That link is no longer supported": "Tautan itu tidak didukung lagi", - "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Dimana laman ini berisi informasi yang dapat dikenal, seperti sebuah ruangan, ID pengguna, data itu dihilangkan sebelum dikirimkan ke server.", + "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Di mana laman ini berisi informasi yang dapat dikenal, seperti sebuah ruangan, ID pengguna, data itu dihilangkan sebelum dikirimkan ke server.", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Anda dapat menggunakan opsi server khusus untuk masuk ke server Matrix lain dengan menentukan URL homeserver yang berbeda. Ini memungkinkan Anda untuk menggunakan %(brand)s dengan akun Matrix yang ada di homeserver yang berbeda.", "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "Izin %(brand)s ditolak untuk mengakses lokasi Anda. Mohon izinkan akses lokasi di pengaturan peramban Anda.", "Developer tools": "Alat pengembang", @@ -3766,6 +3766,32 @@ "To leave, return to this page and use the “Leave the beta” button.": "Untuk keluar, kembali ke laman ini dan gunakan tombol “Tinggalkan beta”.", "Use \"Reply in thread\" when hovering over a message.": "Gunakan \"Balas dalam utasan\" ketika kursor berada di atas pesan.", "How can I start a thread?": "Bagaimana saya dapat memulai sebuah utasan?", - "Threads help keep conversations on-topic and easy to track. Learn more.": "Utasan membantu membuat percakapan dalam topik dan mudah untuk dilacak. Pelajari lebih lanjut.", - "Keep discussions organised with threads.": "Buat diskusi tetap teratur dengan utasan." + "Threads help keep conversations on-topic and easy to track. Learn more.": "Utasan membantu membuat percakapan sesuai topik dan mudah untuk dilacak. Pelajari lebih lanjut.", + "Keep discussions organised with threads.": "Buat diskusi tetap teratur dengan utasan.", + "Give feedback": "Berikan masukan", + "Threads are a beta feature": "Utasan adalah fitur beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Gunakan \"Balas dalam utasan\" ketika kursor ada di atas pesan.", + "sends hearts": "mengirim hati", + "Sends the given message with hearts": "Kirim pesan dengan hati", + "Confirm signing out these devices|one": "Konfirmasi mengeluarkan perangkat ini", + "Confirm signing out these devices|other": "Konfirmasi mengeluarkan perangkat ini", + "Live location ended": "Lokasi langsung berakhir", + "Loading live location...": "Memuat lokasi langsung…", + "View live location": "Tampilkan lokasi langsung", + "Live location enabled": "Lokasi langsung diaktifkan", + "Live location error": "Kesalahan lokasi langsung", + "Live until %(expiryTime)s": "Langsung sampai %(expiryTime)s", + "Ban from room": "Cekal dari ruangan", + "Unban from room": "Batalkan cekalan dari ruangan", + "Ban from space": "Cekal dari space", + "Unban from space": "Batalkan cekalan dari space", + "Yes, enable": "Iya, aktifkan", + "Do you want to enable threads anyway?": "Apakah Anda ingin mengaktifkan utasan?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Homeserver Anda saat ini tidak mendukung utasan, jadi fitur ini mungkin tidak andal. Beberapa pesan yang diutas mungkin tidak tersedia. Pelajari lebih lanjut.", + "Partial Support for Threads": "Sebagian Dukungan untuk Utasan", + "Jump to the given date in the timeline": "Pergi ke tanggal yang diberikan di linimasa", + "Right-click message context menu": "Klik kanan menu konteks pesan", + "Disinvite from room": "Batalkan undangan dari ruangan", + "Remove from space": "Keluarkan dari space", + "Disinvite from space": "Batalkan undangan dari space" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 00c1852db37..fee5111aa60 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3829,5 +3829,17 @@ "%(count)s participants|other": "%(count)s partecipanti", "New video room": "Nuova stanza video", "New room": "Nuova stanza", - "Video rooms (under active development)": "Stanze video (in sviluppo attivo)" + "Video rooms (under active development)": "Stanze video (in sviluppo attivo)", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Le conversazioni aiutano a tenere le discussioni in tema e rintracciabili. Maggiori info.", + "Give feedback": "Lascia feedback", + "Threads are a beta feature": "Le conversazioni sono una funzionalità beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Consiglio: usa \"Rispondi nella conversazione\" passando sopra un messaggio.", + "Threads help keep your conversations on-topic and easy to track.": "Le conversazioni ti aiutano a tenere le tue discussioni in tema e rintracciabili.", + "%(featureName)s Beta feedback": "Feedback %(featureName)s beta", + "Beta feature. Click to learn more.": "Funzionalità beta. Clicca per maggiori informazioni.", + "Beta feature": "Funzionalità beta", + "To leave, return to this page and use the “Leave the beta” button.": "Per uscire, torna in questa pagina e usa il pulsante \"Abbandona la beta\".", + "Use \"Reply in thread\" when hovering over a message.": "Usa \"Rispondi nella conversazione\" passando sopra un messaggio.", + "How can I start a thread?": "Come inizio una conversazione?", + "Keep discussions organised with threads.": "Tieni le discussioni organizzate in conversazioni." } diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index e41eb8a3f65..85c40c682a6 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -217,7 +217,7 @@ "You are now ignoring %(userId)s": "%(userId)sを無視しています", "Stops ignoring a user, showing their messages going forward": "ユーザーの無視を止めて、メッセージを表示", "Unignored user": "無視していないユーザー", - "You are no longer ignoring %(userId)s": "あなたはもはや%(userId)sを無視していません", + "You are no longer ignoring %(userId)s": "あなたは%(userId)sを無視していません", "Define the power level of a user": "ユーザーの権限レベルを規定", "Deops user with given id": "指定されたIDのユーザーを非表示", "Opens the Developer Tools dialog": "開発者ツールダイアログを開く", @@ -1335,7 +1335,7 @@ "Link this email with your account in Settings to receive invites directly in %(brand)s.": "このメールアドレスを設定からあなたのアカウントにリンクすると%(brand)sから直接招待を受け取ることができます。", "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "ルーム %(roomName)s への招待が、アカウントに関連付けられていないメールアドレス %(email)s に送られました", "You can still join it because this is a public room.": "公開ルームなので参加が可能です。", - "Try to join anyway": "無視して参加", + "Try to join anyway": "参加を試みる", "You can only join it with a working invite.": "有効な招待がある場合にのみ参加できます。", "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "招待を認証する際にエラー(%(errcode)s)が発生しました。この情報をルームの管理者に伝えてみてください。", "Something went wrong with your invite to %(roomName)s": "%(roomName)sへの招待に問題が発生しました", @@ -1434,7 +1434,7 @@ "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "現在を使用して、連絡先を検出可能にしています。以下でIDサーバーを変更できます。", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "切断する前に、IDサーバーからメールアドレスと電話番号を削除することを推奨します。", "You are still sharing your personal data on the identity server .": "まだIDサーバー 個人データを共有しています。", - "Disconnect anyway": "無視して切断", + "Disconnect anyway": "切断", "wait and try again later": "しばらく待って、後でもう一度試す", "contact the administrators of identity server ": "IDサーバー の管理者に連絡", "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "ブラウザーのプラグインから、IDサーバーをブロックするかもしれないもの(Privacy Badgerなど)を確認してください", @@ -2707,8 +2707,8 @@ "Other rooms": "他のルーム", "Pin to sidebar": "サイドバーに固定", "Quick settings": "クイック設定", - "Invite anyway": "無視して招待", - "Invite anyway and never warn me again": "無視して招待し、再び警告しない", + "Invite anyway": "招待", + "Invite anyway and never warn me again": "招待し、再び警告しない", "Recovery Method Removed": "復元方法を削除しました", "Failed to remove some rooms. Try again later": "いくつかのルームの削除に失敗しました。後でもう一度やり直してください", "Delete the room address %(alias)s and remove %(name)s from the directory?": "ルームのアドレス %(alias)s を削除して%(name)sをディレクトリから削除しますか?", @@ -2915,7 +2915,7 @@ "Show %(count)s other previews|one": "他%(count)s個のプレビューを表示", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "このユーザーを認証して、信頼済としてマークします。ユーザーを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。", "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "この端末を認証して、信頼済としてマークします。相手の端末を信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。", - "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "以下のMatrix IDのプロフィールを発見できません。無視して招待しますか?", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "以下のMatrix IDのプロフィールを発見できません。招待しますか?", "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "新しい復元方法を設定しなかった場合、攻撃者がアカウントへアクセスしようとしている可能性があります。設定画面にて、すぐにアカウントのパスワードを変更し、新しい復元方法を設定してください。", "General failure": "一般エラー", "Failed to load group members": "グループのメンバーの読み込みに失敗しました", @@ -3496,5 +3496,68 @@ "All thread events created during the experimental period will now be rendered in the room timeline and displayed as replies. This is a one-off transition. Threads are now part of the Matrix specification.": "テスト中に作成されたスレッドに関するイベントは、ルームのタイムライン上では返信として表示されます。これは一度限りの移行です。スレッドはMatrixの仕様の一部になりました。", "Thank you for helping us testing Threads!": "スレッド機能のテストにご協力いただき、ありがとうございました!", "We’ve recently introduced key stability improvements for Threads, which also means phasing out support for experimental Threads.": "スレッド機能の安定性が改善したため、スレッド機能のテスト版のサポートを終了します。", - "Threads are no longer experimental! 🎉": "スレッドは正式版になりました🎉" + "Threads are no longer experimental! 🎉": "スレッドは正式版になりました🎉", + "Do you want to enable threads anyway?": "スレッドを有効にしますか?", + "Yes, enable": "有効にする", + "Live location error": "位置情報(ライブ)のエラー", + "sends hearts": "ハートを送信", + "Confirm signing out these devices|other": "これらの端末からのサインアウトを承認", + "Confirm signing out these devices|one": "この端末からのサインアウトを承認", + "View live location": "位置情報(ライブ)を表示", + "Loading live location...": "位置情報(ライブ)を読み込んでいます…", + "Video rooms (under active development)": "ビデオ通話ルーム(開発中)", + "How can I start a thread?": "スレッドの開始方法", + "Failed to join": "参加に失敗しました", + "The person who invited you has already left, or their server is offline.": "招待した人が既に退出したか、サーバーがオフラインです。", + "The person who invited you has already left.": "招待した人は既に退出しました。", + "Sorry, your homeserver is too old to participate here.": "申し訳ありませんが、あなたのホームサーバーはここに参加するには古すぎます。", + "There was an error joining.": "参加する際にエラーが発生しました。", + "%(value)sm": "%(value)s分", + "%(value)sh": "%(value)s時", + "%(value)sd": "%(value)s日", + "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "このページがルームやユーザーIDなどの特定可能な情報を含んでいる場合は、その情報はサーバーに送信される前に削除されます。", + "Beta feature": "ベータ版の機能", + "Live location enabled": "位置情報が有効です", + "Jump to the given date in the timeline": "タイムラインの指定した日に移動", + "Unban from space": "スペースからのブロックを解除", + "Ban from space": "スペースからブロック", + "Unban from room": "ルームからのブロックを解除", + "Ban from room": "ルームからブロック", + "Copy link": "リンクをコピー", + "%(featureName)s Beta feedback": "%(featureName)sのベータ版のフィードバック", + "Threads are a beta feature": "スレッドはベータ版の機能です", + "Give feedback": "フィードバックを送信", + "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "警告:あなたの個人情報(暗号化の鍵を含む)が、このセッションに保存されています。このセッションの使用を終了するか、他のアカウントにログインしたい場合は、そのデータを消去してください。", + "The user's homeserver does not support the version of the space.": "ユーザーのホームサーバーは、このバージョンのスペースをサポートしていません。", + "User may or may not exist": "ユーザーが存在するか不明です", + "User does not exist": "ユーザーは存在しません", + "User is already in the room": "ユーザーは既にルームに入っています", + "User is already in the space": "ユーザーは既にスペースに入っています", + "User is already invited to the room": "ユーザーは既にルームに招待されています", + "User is already invited to the space": "ユーザーは既にスペースに招待されています", + "You do not have permission to invite people to this space.": "ユーザーをこのスペースに招待する権限がありません。", + "Failed to invite users to %(roomName)s": "ユーザーを%(roomName)sに招待するのに失敗しました", + "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "コミュニティーのリンク(%(groupId)s)にアクセスしようとしています。
    コミュニティー機能はスペースにより置き換えられ、サポート外になりました。スペースに関しては、こちらをご参照ください。", + "That link is no longer supported": "このリンクはサポートされていません", + "%(value)ss": "%(value)s秒", + "You can still join here.": "参加できます。", + "This invite was sent to %(email)s": "招待が%(email)sに送信されました", + "This room or space does not exist.": "このルームまたはスペースは存在しません。", + "This room or space is not accessible at this time.": "このルームまたはスペースは現在アクセスできません。", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "ルームまたはスペースにアクセスする際に%(errcode)sのエラーが発生しました。エラー発生時にこのメッセージが表示されているなら、バグレポートを送信してください。", + "Stop sharing and close": "共有を停止して閉じる", + "An error occured whilst sharing your live location": "位置情報(ライブ)を共有する際にエラーが発生しました", + "New room": "新しいルーム", + "New video room": "新しいビデオ通話ルーム", + "%(count)s participants|other": "%(count)s人の参加者", + "%(count)s participants|one": "1人の参加者", + "Create a video room": "ビデオ通話ルームを作成", + "Create video room": "ビデオ通話ルームを作成", + "Create room": "ルームを作成", + "Threads help keep your conversations on-topic and easy to track.": "スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。", + "Keep discussions organised with threads.": "スレッド機能を使って、会話をまとめましょう。", + "Threads help keep conversations on-topic and easy to track. Learn more.": "スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。詳しく知る。", + "Beta feature. Click to learn more.": "ベータ版の機能です。クリックすると詳細を表示します。", + "Partial Support for Threads": "スレッド機能の部分的サポート", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。詳しく知る。" } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 5b8ab905114..467bb0badaa 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1271,7 +1271,7 @@ "Find a room…": "Zoek een kamer…", "Find a room… (e.g. %(exampleRoom)s)": "Zoek een kamer… (bv. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Als u de kamer niet kunt vinden is het mogelijk privé, vraag dan om een uitnodiging of maak een nieuwe kamer aan.", - "Explore rooms": "Kamers verkennen", + "Explore rooms": "Kamers ontdekken", "Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen", "Clear cache and reload": "Cache wissen en herladen", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit kan niet ongedaan gemaakt worden. Wilt u doorgaan?", @@ -2765,7 +2765,7 @@ "%(seconds)ss left": "%(seconds)s's over", "Change server ACLs": "Wijzig server ACL's", "Show options to enable 'Do not disturb' mode": "Toon opties om de 'Niet storen' modus in te schakelen", - "You can select all or individual messages to retry or delete": "U kunt alles selecteren of per individueel bericht opnieuw verzenden of verwijderen", + "You can select all or individual messages to retry or delete": "U kunt alles selecteren of per individueel bericht opnieuw versturen of verwijderen", "Sending": "Wordt verstuurd", "Retry all": "Alles opnieuw proberen", "Delete all": "Verwijder alles", @@ -2775,7 +2775,7 @@ "Including %(commaSeparatedMembers)s": "Inclusief %(commaSeparatedMembers)s", "View all %(count)s members|one": "1 lid bekijken", "View all %(count)s members|other": "Bekijk alle %(count)s personen", - "Failed to send": "Verzenden is mislukt", + "Failed to send": "Versturen is mislukt", "Enter your Security Phrase a second time to confirm it.": "Voor uw veiligheidswachtwoord een tweede keer in om het te bevestigen.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Kies een kamer of gesprek om hem toe te voegen. Dit is een Space voor u, niemand zal hiervan een melding krijgen. U kan er later meer toevoegen.", "What do you want to organise?": "Wat wilt u organiseren?", @@ -3530,7 +3530,7 @@ "Message pending moderation": "Bericht in afwachting van moderatie", "Message pending moderation: %(reason)s": "Bericht in afwachting van moderatie: %(reason)s", "Remove from room": "Verwijderen uit kamer", - "Failed to remove user": "Kan gebruiker niet verwijderen", + "Failed to remove user": "Kan persoon niet verwijderen", "Remove them from specific things I'm able to": "Verwijder ze van specifieke dingen die ik kan", "Remove them from everything I'm able to": "Verwijder ze van alles wat ik kan", "Remove from %(roomName)s": "Verwijderen uit %(roomName)s", @@ -3541,10 +3541,10 @@ "You don't have permission to view messages from before you joined.": "U heeft geen toestemming om berichten te bekijken voor voordat u lid word.", "You don't have permission to view messages from before you were invited.": "U bent niet gemachtigd om berichten te bekijken van voordat u werd uitgenodigd.", "From a thread": "Uit een conversatie", - "Remove users": "Gebruikers verwijderen", + "Remove users": "Personen verwijderen", "Keyboard": "Toetsenbord", "Widget": "Widget", - "Automatically send debug logs on decryption errors": "Automatisch foutopsporingslogboeken verzenden bij decoderingsfouten", + "Automatically send debug logs on decryption errors": "Automatisch foutopsporingslogboeken versturen bij decoderingsfouten", "Show join/leave messages (invites/removes/bans unaffected)": "Toon deelname/laat berichten (uitnodigingen/verwijderingen/bans onaangetast)", "Enable location sharing": "Locatie delen inschakelen", "Show extensible event representation of events": "Toon uitbreidbare gebeurtenisweergave van gebeurtenissen", @@ -3556,7 +3556,7 @@ "%(senderName)s has shared their location": "%(senderName)s heeft zijn locatie gedeeld", "%(senderName)s removed %(targetName)s": "%(senderName)s verwijderd %(targetName)s", "%(senderName)s removed %(targetName)s: %(reason)s": "%(senderName)s verwijderd %(targetName)s: %(reason)s", - "Removes user with given id from this room": "Verwijdert gebruiker met opgegeven ID uit deze kamer", + "Removes user with given id from this room": "Verwijder persoon met opgegeven ID uit deze kamer", "Previous autocomplete suggestion": "Vorige suggestie voor automatisch aanvullen", "Next autocomplete suggestion": "Volgende suggestie voor automatisch aanvullen", "Previous room or DM": "Vorige kamer of DM", @@ -3587,7 +3587,7 @@ "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!": "Als u weet wat u doet, Element is open-source, bekijk dan zeker onze GitHub (https://github.com/vector-im/element-web/) en draag bij!", "If someone told you to copy/paste something here, there is a high likelihood you're being scammed!": "Als iemand u heeft gezegd iets hier te kopiëren/plakken, is de kans groot dat u wordt opgelicht!", "Wait!": "Wacht!", - "Unable to check if username has been taken. Try again later.": "Kan niet controleren of gebruikersnaam is gebruikt. Probeer het later nog eens.", + "Unable to check if username has been taken. Try again later.": "Kan niet controleren of inlognaam is gebruikt. Probeer het later nog eens.", "This address does not point at this room": "Dit adres verwijst niet naar deze kamer", "Pick a date to jump to": "Kies een datum om naar toe te springen", "Jump to date": "Spring naar datum", @@ -3619,7 +3619,7 @@ "<%(count)s spaces>|other": "<%(count)s spaces>", "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)sverzond een verborgen bericht", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)sverzond %(count)s verborgen berichten", - "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)sverzond %(count)s verborgen bericht", + "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)sverzond verborgen bericht", "%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)sverzond %(count)s verborgen berichten", "%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sheeft een bericht verwijderd", "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sverwijderde %(count)s berichten", @@ -3628,7 +3628,7 @@ "%(severalUsers)schanged the pinned messages for the room %(count)s times.|one": "%(severalUsers)s hebben de vastgezette berichten voor de kamer gewijzigd.", "Maximise": "Maximaliseren", "You do not have permissions to add spaces to this space": "U bent niet gemachtigd om spaces aan deze space toe te voegen", - "Automatically send debug logs when key backup is not functioning": "Automatisch foutopsporingslogboeken verzenden wanneer de sleutelback-up niet werkt", + "Automatically send debug logs when key backup is not functioning": "Automatisch foutopsporingslogboeken versturen wanneer de sleutelback-up niet werkt", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Bedankt voor het proberen van de bèta. Ga alsjeblieft zo gedetailleerd mogelijk in op de details zodat we deze kunnen verbeteren.", "To leave, just return to this page or click on the beta badge when you search.": "Om te vertrekken, keert u gewoon terug naar deze pagina of klikt u op het bèta label tijdens het zoeken.", "How can I leave the beta?": "Hoe kan ik de bèta verlaten?", @@ -3638,5 +3638,206 @@ "A new, quick way to search spaces and rooms you're in.": "Een nieuwe, snelle manier om spaces en kamers te zoeken waarin u zich bevindt.", "The new search": "De nieuwe zoekopdracht", "New search experience": "Nieuwe zoekervaring", - "%(space1Name)s and %(space2Name)s": "%(space1Name)s en %(space2Name)s" + "%(space1Name)s and %(space2Name)s": "%(space1Name)s en %(space2Name)s", + "You do not have permission to invite people to this space.": "U bent niet gemachtigd om mensen voor deze space uit te nodigen.", + "No virtual room for this room": "Geen virtuele ruimte voor deze ruimte", + "Switches to this room's virtual room, if it has one": "Schakelt over naar de virtuele kamer van deze kamer, als die er is", + "Failed to invite users to %(roomName)s": "Kan personen niet uitnodigen voor %(roomName)s", + "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "U probeert toegang te krijgen tot een communitylink (%(groupId)s).
    Communities worden niet langer ondersteund en zijn vervangen door spaces.Lees hier meer over spaces.", + "That link is no longer supported": "Deze link wordt niet langer ondersteund", + "%(value)ss": "%(value)ss", + "%(value)sm": "%(value)sm", + "%(value)sh": "%(value)sh", + "%(value)sd": "%(value)sd", + "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Als deze pagina identificeerbare informatie bevat, zoals een kamer, persoon-ID, worden die gegevens verwijderd voordat ze naar de server worden verzonden.", + "Observe only": "Alleen observeren", + "Requester": "Aanvrager", + "Methods": "Methoden", + "Timeout": "Time-out", + "Phase": "Fase", + "Transaction": "Transactie", + "Cancelled": "Geannuleerd", + "Started": "Begonnen", + "Ready": "Gereed", + "Requested": "Aangevraagd", + "Unsent": "niet verstuurd", + "Edit values": "Bewerk waarden", + "Failed to save settings.": "Kan instellingen niet opslaan.", + "Number of users": "Aantal personen", + "Server": "Server", + "Server Versions": "Serverversies", + "Client Versions": "cliëntversies", + "Failed to load.": "Laden mislukt.", + "Capabilities": "Mogelijkheden", + "Send custom state event": "Aangepaste statusgebeurtenis versturen", + "Failed to send event!": "Kan gebeurtenis niet versturen!", + "Doesn't look like valid JSON.": "Lijkt niet op geldige JSON.", + "Send custom room account data event": "Gegevensgebeurtenis van aangepaste kamer account versturen", + "Send custom account data event": "Aangepaste accountgegevens gebeurtenis versturen", + "Search Dialog": "Dialoogvenster Zoeken", + "Join %(roomAddress)s": "%(roomAddress)s toetreden", + "Export Cancelled": "Export geannuleerd", + "Room ID: %(roomId)s": "Kamer ID: %(roomId)s", + "Server info": "Server info", + "Settings explorer": "Instellingen verkenner", + "Explore account data": "Accountgegevens ontdekken", + "Verification explorer": "Verificatie verkenner", + "View servers in room": "Servers in de kamer bekijken", + "Explore room account data": "Kamer accountgegevens ontdekken", + "Explore room state": "Kamerstatus ontdekken", + "Send custom timeline event": "Aangepaste tijdlijngebeurtenis versturen", + "Create room": "Ruimte aanmaken", + "Create video room": "Videokamer maken", + "Create a video room": "Creëer een videokamer", + "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Schakel het vinkje uit als u ook systeemberichten van deze persoon wilt verwijderen (bijv. lidmaatschapswijziging, profielwijziging...)", + "Preserve system messages": "Systeemberichten behouden", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|one": "U staat op het punt %(count)s bericht te verwijderen door %(user)s. Hierdoor worden ze permanent verwijderd voor iedereen in het gesprek. Wilt u doorgaan?", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|other": "U staat op het punt %(count)s berichten te verwijderen door %(user)s. Hierdoor worden ze permanent verwijderd voor iedereen in het gesprek. Wilt u doorgaan?", + "%(featureName)s Beta feedback": "%(featureName)s Bèta-feedback", + "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "Help ons problemen te identificeren en %(analyticsOwner)s te verbeteren door anonieme gebruiksgegevens te delen. Om inzicht te krijgen in hoe mensen meerdere apparaten gebruiken, genereren we een willekeurige identificatie die door uw apparaten wordt gedeeld.", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "U kunt de aangepaste serveropties gebruiken om u aan te melden bij andere Matrix-servers door een andere server-URL op te geven. Hierdoor kunt u %(brand)s gebruiken met een bestaand Matrix-account op een andere thuisserver.", + "Results are only revealed when you end the poll": "Resultaten worden pas onthuld als u de poll beëindigt", + "Voters see results as soon as they have voted": "Kiezers zien resultaten zodra ze hebben gestemd", + "Closed poll": "Gesloten poll", + "Open poll": "Start poll", + "Poll type": "Poll type", + "Edit poll": "Bewerk poll", + "%(oneUser)schanged the pinned messages for the room %(count)s times|one": "%(oneUser)sheeft de vastgezette berichten voor de kamer gewijzigd", + "%(oneUser)schanged the pinned messages for the room %(count)s times|other": "%(oneUser)sheeft de vastgezette berichten voor de kamer %(count)s keer gewijzigd", + "%(severalUsers)schanged the pinned messages for the room %(count)s times|one": "%(severalUsers)shebben de vastgezette berichten voor de kamer gewijzigd", + "%(severalUsers)schanged the pinned messages for the room %(count)s times|other": "%(severalUsers)sheeft de vastgezette berichten voor de kamer %(count)s keer gewijzigd", + "What location type do you want to share?": "Welk locatietype wilt u delen?", + "Drop a Pin": "Zet een pin neer", + "My live location": "Mijn live locatie", + "My current location": "Mijn huidige locatie", + "%(displayName)s's live location": "De live locatie van %(displayName)s", + "We couldn't send your location": "We kunnen uw locatie niet versturen", + "%(brand)s could not send your location. Please try again later.": "%(brand)s kan uw locatie niet versturen. Probeer het later opnieuw.", + "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "%(brand)s heeft geen toestemming gekregen om uw locatie op te halen. Sta locatietoegang toe in uw browserinstellingen.", + "Click to drop a pin": "Klik om een pin neer te zetten", + "Click to move the pin": "Klik om de pin te verplaatsen", + "Share for %(duration)s": "Delen voor %(duration)s", + "Results will be visible when the poll is ended": "Resultaten zijn zichtbaar wanneer de poll is afgelopen", + "Sorry, you can't edit a poll after votes have been cast.": "Sorry, u kunt een poll niet bewerken nadat er gestemd is.", + "Can't edit poll": "Kan poll niet bewerken", + "Shared a location: ": "Een locatie gedeeld: ", + "Shared their location: ": "Hun locatie gedeeld: ", + "Unable to load map": "Kan kaart niet laden", + "Click": "Klik", + "Expand quotes": "Citaten uitvouwen", + "Collapse quotes": "Citaten invouwen", + "Beta feature. Click to learn more.": "Bètafunctie. Klik om meer te leren.", + "Beta feature": "Bètafunctie", + "Can't create a thread from an event with an existing relation": "Kan geen discussie maken van een gebeurtenis met een bestaande relatie", + "Pinned": "Vastgezet", + "Open thread": "Open discussie", + "%(count)s participants|one": "1 deelnemer", + "%(count)s participants|other": "%(count)s deelnemers", + "Connected": "Verbonden", + "Video": "Video", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s is geretourneerd tijdens een poging om toegang te krijgen tot de kamer of space. Als u denkt dat je dit bericht ten onrechte ziet, dien dan een bugrapport in.", + "Try again later, or ask a room or space admin to check if you have access.": "Probeer het later opnieuw of vraag een kamer- of space beheerder om te controleren of u toegang heeft.", + "This room or space is not accessible at this time.": "Deze kamer of space is op dit moment niet toegankelijk.", + "Are you sure you're at the right place?": "Weet u zeker dat je op de goede locatie bent?", + "This room or space does not exist.": "Deze kamer of space bestaat niet.", + "There's no preview, would you like to join?": "Er is geen preview, wilt u toetreden?", + "This invite was sent to %(email)s": "De uitnodiging is verzonden naar %(email)s", + "This invite was sent to %(email)s which is not associated with your account": "Deze uitnodiging is verzonden naar %(email)s die niet is gekoppeld aan uw account", + "You can still join here.": "U kunt hier nog toetreden.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "Er is een fout (%(errcode)s) geretourneerd tijdens het valideren van uw uitnodiging. U kunt proberen deze informatie door te geven aan de persoon die u heeft uitgenodigd.", + "Something went wrong with your invite.": "Er is iets misgegaan met uw uitnodiging.", + "You were banned by %(memberName)s": "U bent verbannen door %(memberName)s", + "Forget this space": "Vergeet deze space", + "You were removed by %(memberName)s": "U bent verwijderd door %(memberName)s", + "Loading preview": "Voorbeeld laden", + "Joining …": "Deelnemen…", + "Currently removing messages in %(count)s rooms|one": "Momenteel berichten in %(count)s kamer aan het verwijderen", + "Currently removing messages in %(count)s rooms|other": "Momenteel berichten in %(count)s kamers aan het verwijderen", + "New video room": "Nieuwe video kamer", + "New room": "Nieuwe kamer", + "Busy": "Bezet", + "Remove messages sent by me": "Door mij verzonden berichten verwijderen", + "View older version of %(spaceName)s.": "Bekijk oudere versie van %(spaceName)s.", + "Upgrade this space to the recommended room version": "Upgrade deze ruimte naar de aanbevolen kamerversie", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Foutopsporingslogboeken bevatten toepassingsgebruiksgegevens, waaronder uw inlognaam, de ID's of aliassen van de kamers die u heeft bezocht, met welke UI-elementen u voor het laatst interactie heeft gehad en de inlognamen van andere personen. Ze bevatten geen berichten.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "Als u een bug via GitHub heeft ingediend, kunnen foutopsporingslogboeken ons helpen het probleem op te sporen. ", + "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.": "Spaces zijn een nieuwe manier om kamers en mensen te groeperen. Wat voor ruimte wilt u aanmaken? U kunt dit later wijzigen.", + "Match system": "Match systeem", + "Developer tools": "Ontwikkelaarstools", + "sends hearts": "stuurt hartjes", + "Sends the given message with hearts": "Stuurt het bericht met hartjes", + "Insert a trailing colon after user mentions at the start of a message": "Voeg een dubbele punt in nadat de persoon het aan het begin van een bericht heeft vermeld", + "Show polls button": "Toon polls-knop", + "Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Live locatie delen - deel huidige locatie (actieve ontwikkeling, en tijdelijk, locaties blijven bestaan in kamergeschiedenis)", + "Location sharing - pin drop (under active development)": "Locatie delen - pin drop (in actieve ontwikkeling)", + "Show current avatar and name for users in message history": "Toon huidige avatar en naam voor persoon in berichtgeschiedenis", + "Video rooms (under active development)": "Videokamers (in actieve ontwikkeling)", + "To leave, return to this page and use the “Leave the beta” button.": "Om te verlaten, keer terug naar deze pagina en gebruik de knop \"Verlaat de bèta\".", + "Use \"Reply in thread\" when hovering over a message.": "Gebruik \"Beantwoorden in gesprek\" wanneer u de muisaanwijzer op een bericht plaatst.", + "How can I start a thread?": "Hoe kan ik een discussie starten?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Discussies helpen om gesprekken on-topic te houden en gemakkelijk te volgen. Meer informatie.", + "Keep discussions organised with threads.": "Houd discussies georganiseerd met discussielijnen.", + "Failed to join": "Kan niet deelnemen", + "The person who invited you has already left, or their server is offline.": "De persoon die u heeft uitgenodigd is al vertrokken, of zijn server is offline.", + "The person who invited you has already left.": "De persoon die u heeft uitgenodigd is al vertrokken.", + "Sorry, your homeserver is too old to participate here.": "Sorry, uw server is te oud om hier deel te nemen.", + "There was an error joining.": "Er is een fout opgetreden bij het deelnemen.", + "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.": "%(brand)s is experimenteel in een mobiele webbrowser. Gebruik onze gratis native app voor een betere ervaring en de nieuwste functies.", + "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "Deze server is niet correct geconfigureerd om kaarten weer te geven, of de geconfigureerde kaartserver is mogelijk onbereikbaar.", + "The user's homeserver does not support the version of the space.": "De server van de persoon ondersteunt de versie van de ruimte niet.", + "This homeserver is not configured to display maps.": "Deze server is niet geconfigureerd om kaarten weer te geven.", + "User may or may not exist": "Persoon kan wel of niet bestaan", + "User does not exist": "Persoon bestaat niet", + "User is already invited to the room": "Persoon is al uitgenodigd voor de kamer", + "User is already in the room": "Persoon is al in de kamer", + "User is already in the space": "Persoon is al in de space", + "User is already invited to the space": "Persoon is al uitgenodigd voor de space", + "Toggle Code Block": "Codeblok wisselen", + "Toggle Link": "Koppeling wisselen", + "Accessibility": "Toegankelijkheid", + "Event ID: %(eventId)s": "Gebeurtenis ID: %(eventId)s", + "Give feedback": "Feedback geven", + "Threads are a beta feature": "Discussies zijn een bètafunctie", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: gebruik \"Antwoord in discussie\" wanneer u de muisaanwijzer op een bericht plaatst.", + "Threads help keep your conversations on-topic and easy to track.": "Discussies helpen u gesprekken on-topic te houden en gemakkelijk bij te houden.", + "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "Reageer op een lopende discussie of gebruik \"%(replyInThread)s\" wanneer u de muisaanwijzer op een bericht plaatst om een nieuwe te starten.", + "We'll create rooms for each of them.": "We zullen kamers voor elk van hen maken.", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "Als u de kamer die u zoekt niet kunt vinden, vraag dan om een uitnodiging of maak een nieuwe kamer aan.", + "This will be a one-off transition, as threads are now part of the Matrix specification.": "Dit zal een eenmalige overgang zijn, aangezien discussies nu deel uitmaken van de Matrix-specificatie.", + "As we prepare for it, we need to make some changes: threads created before this point will be displayed as regular replies.": "Terwijl we ons erop voorbereiden, moeten we enkele wijzigingen aanbrengen: discussies die vóór dit punt zijn gemaakt, worden weergegeven als gewone antwoorden.", + "We're getting closer to releasing a public Beta for Threads.": "We komen dichter bij de release van een publieke bètaversie voor Discussies.", + "Threads Approaching Beta 🎉": "Discussies naderen bèta 🎉", + "Stop sharing and close": "Stop met delen en sluit", + "Stop sharing": "Stop delen", + "An error occurred while stopping your live location, please try again": "Er is een fout opgetreden bij het stoppen van uw live locatie, probeer het opnieuw", + "An error occured whilst sharing your live location, please try again": "Er is een fout opgetreden bij het delen van uw live locatie, probeer het opnieuw", + "%(timeRemaining)s left": "%(timeRemaining)s over", + "You are sharing your live location": "U deelt uw live locatie", + "An error occured whilst sharing your live location": "Er is een fout opgetreden bij het delen van uw live locatie", + "No verification requests found": "Geen verificatieverzoeken gevonden", + "Open user settings": "Open persooninstellingen", + "Switch to space by number": "Overschakelen naar space op nummer", + "Next recently visited room or space": "Volgende recent bezochte kamer of space", + "Previous recently visited room or space": "Vorige recent bezochte kamer of ruimte", + "Live location enabled": "Live locatie ingeschakeld", + "Live location error": "Live locatie error", + "Live location ended": "Live locatie beëindigd", + "Loading live location...": "Live locatie laden...", + "Live until %(expiryTime)s": "Live tot %(expiryTime)s", + "View live location": "Bekijk live locatie", + "Ban from room": "Verban van kamer", + "Unban from room": "Ontban van kamer", + "Ban from space": "Verban van space", + "Unban from space": "Unban van space", + "Disinvite from room": "Uitnodiging van kamer afwijzen", + "Remove from space": "Verwijder van space", + "Disinvite from space": "Uitnodiging van space afwijzen", + "Confirm signing out these devices|one": "Uitloggen van dit apparaat bevestigen", + "Confirm signing out these devices|other": "Uitloggen van deze apparaten bevestigen", + "Yes, enable": "Ja, inschakelen", + "Do you want to enable threads anyway?": "Wil je toch discussies inschakelen?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Uw server ondersteunt momenteel geen discussies, dus deze functie kan onbetrouwbaar zijn. Sommige berichten in een discussie zijn mogelijk niet betrouwbaar beschikbaar. Meer informatie.", + "Partial Support for Threads": "Gedeeltelijke ondersteuning voor Discussies", + "Right-click message context menu": "Rechtermuisknop op het bericht voor opties", + "Jump to the given date in the timeline": "Spring naar de opgegeven datum in de tijdlijn" } diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 8f9de471400..37804f2f1c0 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -387,7 +387,7 @@ "Failed to reject invitation": "Nepodarilo sa odmietnuť pozvanie", "Are you sure you want to leave the room '%(roomName)s'?": "Ste si istí, že chcete opustiť miestnosť '%(roomName)s'?", "Signed Out": "Ste odhlásení", - "For security, this session has been signed out. Please sign in again.": "Kôli bezpečnosti ste boli odhlásení z tejto relácie. Prosím, prihláste sa znovu.", + "For security, this session has been signed out. Please sign in again.": "Z bezpečnostných dôvodov bola táto relácia odhlásená. Prosím, prihláste sa znova.", "Logout": "Odhlásiť sa", "Your Communities": "Vaše komunity", "You're not currently a member of any communities.": "V súčasnosti nie ste členom žiadnej komunity.", @@ -830,69 +830,69 @@ "Verify this user by confirming the following emoji appear on their screen.": "Overte tohto používateľa potvrdením, že sa na jeho obrazovke zobrazujú nasledujúce emotikony.", "Verify this user by confirming the following number appears on their screen.": "Overte tohoto používateľa tým, že zistíte, či sa na jeho obrazovke objaví nasledujúce číslo.", "Unable to find a supported verification method.": "Nie je možné nájsť podporovanú metódu overenia.", - "Dog": "Hlava psa", - "Cat": "Hlava mačky", - "Lion": "Hlava leva", + "Dog": "Pes", + "Cat": "Mačka", + "Lion": "Lev", "Horse": "Kôň", - "Unicorn": "Hlava jednorožca", - "Pig": "Hlava Prasaťa", + "Unicorn": "Jednorožec", + "Pig": "Prasa", "Elephant": "Slon", - "Rabbit": "Hlava Zajaca", - "Panda": "Hlava Pandy", + "Rabbit": "Zajac", + "Panda": "Panda", "Rooster": "Kohút", "Penguin": "Tučniak", "Turtle": "Korytnačka", "Fish": "Ryba", "Octopus": "Chobotnica", "Butterfly": "Motýľ", - "Flower": "Tulipán", - "Tree": "Listnatý strom", + "Flower": "Kvet", + "Tree": "Strom", "Cactus": "Kaktus", "Mushroom": "Huba", "Globe": "Zemeguľa", - "Moon": "Polmesiac", + "Moon": "Mesiac", "Cloud": "Oblak", "Fire": "Oheň", "Banana": "Banán", - "Apple": "Červené jablko", + "Apple": "Jablko", "Strawberry": "Jahoda", - "Corn": "Kukuričný klas", + "Corn": "Kukurica", "Pizza": "Pizza", - "Cake": "Narodeninová torta", - "Heart": "Červené srdce", - "Smiley": "Škeriaca sa tvár", + "Cake": "Torta", + "Heart": "Srdce", + "Smiley": "Smajlík", "Robot": "Robot", - "Hat": "Cylinder", + "Hat": "Klobúk", "Glasses": "Okuliare", - "Spanner": "Francúzsky kľúč", - "Santa": "Santa Claus", - "Thumbs up": "palec nahor", + "Spanner": "Vidlicový kľúč", + "Santa": "Mikuláš", + "Thumbs up": "Palec nahor", "Umbrella": "Dáždnik", "Hourglass": "Presýpacie hodiny", "Clock": "Budík", - "Gift": "Zabalený darček", + "Gift": "Darček", "Light bulb": "Žiarovka", - "Book": "Zatvorená kniha", + "Book": "Kniha", "Pencil": "Ceruzka", - "Paperclip": "Sponka na papier", + "Paperclip": "Kancelárska sponka", "Scissors": "Nožnice", "Key": "Kľúč", "Hammer": "Kladivo", "Telephone": "Telefón", - "Flag": "Kockovaná zástava", - "Train": "Rušeň", + "Flag": "Zástava", + "Train": "Vlak", "Bicycle": "Bicykel", "Aeroplane": "Lietadlo", "Rocket": "Raketa", "Trophy": "Trofej", - "Ball": "Futbal", + "Ball": "Lopta", "Guitar": "Gitara", "Trumpet": "Trúbka", - "Bell": "Zvon", + "Bell": "Zvonec", "Anchor": "Kotva", "Headphones": "Slúchadlá", "Folder": "Fascikel", - "Pin": "Pripnúť", + "Pin": "Špendlík", "Yes": "Áno", "No": "Nie", "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Poslali sme vám email, aby sme mohli overiť vašu adresu. Postupujte podľa odoslaných inštrukcií a potom klepnite na nižšie zobrazené tlačidlo.", @@ -1273,7 +1273,7 @@ "They match": "Zhodujú sa", "They don't match": "Nezhodujú sa", "To be secure, do this in person or use a trusted way to communicate.": "Aby ste si boli istý, urobte to osobne alebo použite dôveryhodný spôsob komunikácie.", - "Lock": "Zámok", + "Lock": "Zámka", "If you can't scan the code above, verify by comparing unique emoji.": "Ak sa vám nepodarí naskenovať uvedený kód, overte pomocou porovnania jedinečných emotikonov.", "Verify by comparing unique emoji.": "Overenie porovnaním jedinečnej kombinácie emotikonov.", "Verify by emoji": "Overiť pomocou emotikonov", @@ -1333,7 +1333,7 @@ "Size must be a number": "Veľkosť musí byť číslo", "Custom font size can only be between %(min)s pt and %(max)s pt": "Vlastná veľkosť písma môže byť len v rozmedzí %(min)s pt až %(max)s pt", "Help us improve %(brand)s": "Pomôžte nám zlepšovať %(brand)s", - "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Posielať anonymné dáta o používaní, ktoré nám pomôžu zlepšiť %(brand)s. Toto bude vyžadovať sušienku.", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Posielať anonymné dáta o používaní, ktoré nám pomôžu zlepšiť %(brand)s. Toto bude vyžadovať koláčik.", "Your homeserver has exceeded its user limit.": "Na vašom domovskom serveri bol prekročený limit počtu používateľov.", "Your homeserver has exceeded one of its resource limits.": "Na vašom domovskom serveri bol prekročený jeden z limitov systémových zdrojov.", "Contact your server admin.": "Kontaktujte svojho administrátora serveru.", @@ -1394,8 +1394,8 @@ "View rules": "Zobraziť pravidlá", "You are currently subscribed to:": "Aktuálne odoberáte:", "⚠ These settings are meant for advanced users.": "⚠ Tieto nastavenia sú určené pre pokročilých používateľov.", - "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Pridajte používateľov a servery, ktorých chcete ignorovať. Použite hviezdičku '*', aby %(brand)s ju priradil každému symbolu. Napríklad @bot:* by odignoroval všetkých používateľov s menom 'bot' na akomkoľvek serveri.", - "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorovanie ľudí sa vykonáva prostredníctvom zoznamov zákazov, ktoré obsahujú pravidlá pre zakazovanie. Prihlásenie sa na zoznam zákazov znamená, že používatelia/servery zablokované týmto zoznamom budú pred vami skryté.", + "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Pridajte používateľov a servery, ktorých chcete ignorovať. Použite hviezdičku '*', aby ju %(brand)s priradil každému symbolu. Napríklad @bot:* by odignoroval všetkých používateľov s menom 'bot' na akomkoľvek serveri.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorovanie ľudí sa vykonáva prostredníctvom zoznamov zákazov, ktoré obsahujú pravidlá pre zakazovanie. Pridanie na zoznam zákazov znamená, že používatelia/servery na tomto zozname pred vami skryté.", "Personal ban list": "Osobný zoznam zákazov", "Server or user ID to ignore": "Server alebo ID používateľa na odignorovanie", "eg: @bot:* or example.org": "napr.: @bot:* alebo napriklad.sk", @@ -3749,5 +3749,41 @@ "%(count)s participants|other": "%(count)s účastníkov", "New video room": "Nová video miestnosť", "New room": "Nová miestnosť", - "Video rooms (under active development)": "Video miestnosti (v aktívnom vývoji)" + "Video rooms (under active development)": "Video miestnosti (v aktívnom vývoji)", + "Give feedback": "Poskytnúť spätnú väzbu", + "Threads are a beta feature": "Vlákna sú beta funkciou", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Použite položku \"Odpovedať vo vlákne\", keď prejdete nad správu.", + "Threads help keep your conversations on-topic and easy to track.": "Vlákna pomáhajú udržiavať konverzácie v téme a uľahčujú ich sledovanie.", + "%(featureName)s Beta feedback": "%(featureName)s Beta spätná väzba", + "Beta feature. Click to learn more.": "Beta funkcia. Kliknutím sa dozviete viac.", + "Beta feature": "Beta funkcia", + "To leave, return to this page and use the “Leave the beta” button.": "Ak chcete odísť, vráťte sa na túto stránku a použite tlačidlo \"Opustiť beta verziu\".", + "Use \"Reply in thread\" when hovering over a message.": "Použite položku \"Odpovedať vo vlákne\", keď prejdete nad správu.", + "How can I start a thread?": "Ako môžem začať vlákno?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Vlákna pomáhajú udržiavať konverzácie v téme a uľahčujú ich sledovanie. Zistite viac.", + "Keep discussions organised with threads.": "Udržujte diskusie organizované pomocou vlákien.", + "sends hearts": "pošle srdiečka", + "Sends the given message with hearts": "Odošle danú správu so srdiečkami", + "Confirm signing out these devices|one": "Potvrďte odhlásenie z tohto zariadenia", + "Confirm signing out these devices|other": "Potvrdiť odhlásenie týchto zariadení", + "Live location ended": "Ukončenie polohy v reálnom čase", + "Loading live location...": "Načítavanie polohy v reálnom čase...", + "View live location": "Zobraziť polohu v reálnom čase", + "Live until %(expiryTime)s": "Poloha v reálnom čase do %(expiryTime)s", + "Live location enabled": "Poloha v reálnom čase zapnutá", + "Live location error": "Chyba polohy v reálnom čase", + "Ban from room": "Zakázať vstup do miestnosti", + "Unban from room": "Zrušiť zákaz vstupu do miestnosti", + "Unban from space": "Zrušiť zákaz vstupu do priestoru", + "Ban from space": "Zakázať vstup do priestoru", + "Yes, enable": "Áno, povoliť", + "Do you want to enable threads anyway?": "Chcete aj napriek tomu povoliť vlákna?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Váš domovský server v súčasnosti nepodporuje vlákna, takže táto funkcia môže byť nespoľahlivá. Niektoré správy vo vláknach nemusia byť spoľahlivo dostupné. Získajte viac informácií.", + "Partial Support for Threads": "Čiastočná podpora vlákien", + "Jump to the given date in the timeline": "Prejsť na zadaný dátum na časovej osi", + "Copy link": "Kopírovať odkaz", + "Right-click message context menu": "Kontextové menu správy pravým kliknutím", + "Disinvite from room": "Zrušiť pozvánku z miestnosti", + "Remove from space": "Odstrániť z priestoru", + "Disinvite from space": "Zrušiť pozvánku z priestoru" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 08729ea7387..16c70db0f6c 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3820,5 +3820,17 @@ "%(count)s participants|other": "%(count)s pjesëmarrës", "New video room": "Dhomë e re me video", "New room": "Dhomë e re", - "Video rooms (under active development)": "Dhoma me video (nën zhvillim aktiv)" + "Video rooms (under active development)": "Dhoma me video (nën zhvillim aktiv)", + "Give feedback": "Jepni përshtypjet", + "Threads are a beta feature": "Rrjedhat janë një veçori beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Ndihmëz: Përdorni “Përgjigjuni në rrjedhë”, teksa kaloni kursorin sipër një mesazhi.", + "Threads help keep your conversations on-topic and easy to track.": "Rrjedhat ndihmojnë që të mbahen bisedat tuaja brenda temës dhe të ndiqen kollaj.", + "%(featureName)s Beta feedback": "Përshtypje për %(featureName)s Beta", + "Beta feature. Click to learn more.": "Veçori në version beta. Klikoni për të mësuar më tepër.", + "Beta feature": "Veçori në version beta", + "To leave, return to this page and use the “Leave the beta” button.": "Për ta braktisur, kthehuni te kjo faqe dhe përdorni butonin “Braktise beta-n”.", + "Use \"Reply in thread\" when hovering over a message.": "Përdorni “Përgjigjuni në rrjedhë”, teksa kaloni kursorin sipër një mesazhi.", + "How can I start a thread?": "Si mund të nis një rrjedhë?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Rrjedhat ndihmojnë që të mbahen bisedat tuaja brenda temës dhe të ndiqen kollaj. Mësoni më tepër.", + "Keep discussions organised with threads.": "Mbajini diskutimet të sistemuara në rrjedha." } diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index ee86594d175..6351262b59e 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3703,5 +3703,29 @@ "This room or space is not accessible at this time.": "Det är rummet eller utrymmet är inte åtkomligt för tillfället.", "Are you sure you're at the right place?": "Är du säker på att du är på rätt ställe?", "This room or space does not exist.": "Det här rummet eller utrymmet finns inte.", - "There's no preview, would you like to join?": "Det finns ingen förhandsgranskning, vill du gå med?" + "There's no preview, would you like to join?": "Det finns ingen förhandsgranskning, vill du gå med?", + "Shared a location: ": "Delade en plats: ", + "Shared their location: ": "Delade sin plats: ", + "Unable to load map": "Kunde inte ladda kartan", + "Click": "Klicka", + "Expand quotes": "Expandera citat", + "Collapse quotes": "Kollapsa citat", + "Beta feature. Click to learn more.": "Betafunktion. Klicka för att läsa mer.", + "Beta feature": "Betafunktion", + "Can't create a thread from an event with an existing relation": "Kan inte skapa tråd från en händelse med en existerande relation", + "sends hearts": "skicka hjärtan", + "Sends the given message with hearts": "Skickar det givna meddelandet med hjärtan", + "To leave, return to this page and use the “Leave the beta” button.": "För att lämna, återvänd till den här sidan och använd \"Lämna betan\"-knappen.", + "Use \"Reply in thread\" when hovering over a message.": "Använd \"Svara i tråd\" när du håller pekaren över ett meddelande.", + "How can I start a thread?": "Hur kan jag starta en tråd?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Trådar hjälper till att hålla diskussioner till ämnet och lätta att hålla reda på. Läs mer.", + "Keep discussions organised with threads.": "Håll diskussioner organiserade med trådar.", + "Confirm signing out these devices|one": "Bekräfta utloggning av denna enhet", + "Confirm signing out these devices|other": "Bekräfta utloggning av dessa enheter", + "Yes, enable": "Ja, aktivera", + "Do you want to enable threads anyway?": "Vill du aktivera trådar iallafall?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Din hemserver stöder för närvarande inte trådar, så den här funktionen kan vara opålitlig. Vissa trådade kanske inte är tillgängliga. Läs mer.", + "Partial Support for Threads": "Delvist stöd för trådar", + "Right-click message context menu": "Kontextmeny vid högerklick på meddelande", + "Jump to the given date in the timeline": "Hoppa till det angivna datumet i tidslinjen" } diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 5019984872e..667242cb524 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -2375,7 +2375,7 @@ "Confirm signing out these devices": "Підтвердьте вихід з цих пристроїв", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Підтвердьте вихід із цих пристроїв за допомогою єдиного входу, щоб довести вашу справжність.", "To continue, use Single Sign On to prove your identity.": "Щоб продовжити, скористайтеся єдиним входом для підтвердження особи.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Підтвердьте вхід на це пристрій за допомогою єдиного входу, щоб підтвердити вашу особу.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Підтвердьте вихід із цього пристрою за допомогою єдиного входу, щоб підтвердити вашу особу.", "Click the button below to confirm signing out these devices.|one": "Натисніть кнопку внизу, щоб підтвердити вихід із цього пристрою.", "This device": "Цей пристрій", "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Ваші приватні повідомлення, зазвичай, зашифровані, але ця кімната — ні. Зазвичай це пов'язано з непідтримуваним пристроєм або використаним методом, наприклад, запрошення електронною поштою.", @@ -2428,7 +2428,7 @@ "Quick settings": "Швидкі налаштування", "Home options": "Параметри домівки", "Files": "Файли", - "Export chat": "Експортувати чат", + "Export chat": "Експортувати бесіду", "View in room": "Дивитися в кімнаті", "Copy link to thread": "Копіювати лінк треду", "Thread options": "Параметри треду", @@ -3780,5 +3780,44 @@ "%(count)s participants|other": "%(count)s учасників", "New video room": "Нова відеокімната", "New room": "Нова кімната", - "Video rooms (under active development)": "Відеокімнати (в активній розробці)" + "Video rooms (under active development)": "Відеокімнати (в активній розробці)", + "Give feedback": "Залиште відгук", + "Threads are a beta feature": "Треди — бетафункція", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Підказка: наведіть курсор на повідомлення й натисніть «Відповісти в тред».", + "Threads help keep your conversations on-topic and easy to track.": "Треди допомагають підтримувати розмови за темою та за ними легко стежити.", + "%(featureName)s Beta feedback": "%(featureName)s — відгук про бетаверсію", + "Beta feature. Click to learn more.": "Бетафункція. Натисніть, щоб дізнатися більше.", + "Beta feature": "Бетафункція", + "To leave, return to this page and use the “Leave the beta” button.": "Щоб вийти, поверніться до цієї сторінки й натисніть «Вийти з бета-тестування».", + "Use \"Reply in thread\" when hovering over a message.": "Наведіть курсор на повідомлення й натисніть «Відповісти в тред».", + "How can I start a thread?": "Як створити тред?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Треди допомагають розмежовувати розмови на різні теми. Дізнайтеся більше.", + "Keep discussions organised with threads.": "Спілкуйтеся за темою в тредах.", + "sends hearts": "надсилає сердечка", + "Sends the given message with hearts": "Надсилає це повідомлення з сердечками", + "Confirm signing out these devices|one": "Підтвердьте вихід із цього пристрою", + "Confirm signing out these devices|other": "Підтвердьте вихід із цих пристроїв", + "Live location ended": "Показ місцеперебування наживо завершено", + "Loading live location...": "Завантаження місцеперебування наживо...", + "View live location": "Показувати місцеперебування наживо", + "Partial Support for Threads": "Часткова підтримка тредів", + "Jump to the given date in the timeline": "Перейти до вказаної дати в стрічці", + "Live location enabled": "Показ місцеперебування наживо ввімкнено", + "Live location error": "Помилка показу місцеперебування наживо", + "Live until %(expiryTime)s": "Наживо до %(expiryTime)s", + "Yes, enable": "Так, увімкнути", + "Do you want to enable threads anyway?": "Усе одно хочете увімкнути треди?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Ваш домашній сервер не підтримує треди, тому ця функція може бути неповносправною. Деякі треди повідомлень можуть бути не доступними. Докладніше.", + "Ban from space": "Заблокувати у просторі", + "Unban from space": "Розблокувати у просторі", + "Ban from room": "Заблокувати в кімнаті", + "Unban from room": "Розблокувати в кімнаті", + "Right-click message context menu": "Права кнопка миші — контекстне меню повідомлення", + "Use “%(replyInThread)s” when hovering over a message.": "Застосовувати «%(replyInThread)s» після наведення вказівника на повідомлення.", + "Tip: Use “%(replyInThread)s” when hovering over a message.": "Порада: Використовуйте «%(replyInThread)s» навівши вказівник на повідомлення.", + "Disinvite from room": "Відкликати запрошення до кімнати", + "Remove from space": "Вилучити з простору", + "Disinvite from space": "Відкликати запрошення до простору", + "To leave, return to this page and use the “%(leaveTheBeta)s” button.": "Щоб вийти, поверніться на цю сторінку й натисніть кнопку «%(leaveTheBeta)s».", + "No live locations": "Передавання місцеперебування наживо відсутні" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 507a203bb40..8bc0d27e04e 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3822,5 +3822,48 @@ "An error occurred while stopping your live location, please try again": "停止您的即時位置時發生錯誤,請再試一次", "Stop sharing and close": "停止分享並關閉", "An error occured whilst sharing your live location, please try again": "分享您的即時位置時發生錯誤,請重試", - "An error occured whilst sharing your live location": "分享您的即時位置時發生錯誤" + "An error occured whilst sharing your live location": "分享您的即時位置時發生錯誤", + "Give feedback": "給予回饋", + "Threads are a beta feature": "討論串是測試版功能", + "Tip: Use \"Reply in thread\" when hovering over a message.": "秘訣:在游標停於訊息之上時使用「在討論串中回覆」。", + "Threads help keep your conversations on-topic and easy to track.": "討論串可讓您的對話不離題且易於追蹤。", + "Create room": "建立聊天室", + "Create video room": "建立視訊聊天室", + "Create a video room": "建立視訊聊天室", + "%(featureName)s Beta feedback": "%(featureName)s 測試版回饋", + "Beta feature. Click to learn more.": "測試版功能。點擊以取得更多資訊。", + "Beta feature": "測試版功能", + "%(count)s participants|one": "1 個參與者", + "%(count)s participants|other": "%(count)s 個參與者", + "New video room": "新視訊聊天室", + "New room": "新聊天室", + "Video rooms (under active development)": "視訊聊天室(仍在積極開發中)", + "To leave, return to this page and use the “Leave the beta” button.": "若要離開,返回此頁面並使用「離開測試版」按鈕。", + "Use \"Reply in thread\" when hovering over a message.": "在游標停於訊息之上時使用「在討論串中回覆」。", + "How can I start a thread?": "我要如何啟動討論串?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "討論串讓對話不離題且易於追蹤。取得更多資訊。", + "Keep discussions organised with threads.": "透過討論串讓討論保持有條不紊。", + "sends hearts": "傳送愛心", + "Sends the given message with hearts": "與愛心一同傳送指定的訊息", + "Live location ended": "即時位置已結束", + "Loading live location...": "正在載入即時位置……", + "View live location": "檢視即時位置", + "Confirm signing out these devices|one": "確認登出此裝置", + "Confirm signing out these devices|other": "確認登出這些裝置", + "Live location enabled": "即時位置已啟用", + "Live location error": "即時位置錯誤", + "Live until %(expiryTime)s": "即時分享直到 %(expiryTime)s", + "Ban from room": "從聊天室封鎖", + "Unban from room": "從聊天室取消封鎖", + "Ban from space": "從空間封鎖", + "Unban from space": "從空間取消封鎖", + "Yes, enable": "是的,請啟用", + "Do you want to enable threads anyway?": "您無論如何都想要啟用討論串嗎?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "您的家伺服器目前不支援討論串,所以此功能可能不可靠。部份討論串訊息可能無法可靠地使用。取得更多資訊。", + "Partial Support for Threads": "部份支援討論串", + "Jump to the given date in the timeline": "跳至時間軸中指定的日期", + "Disinvite from room": "從聊天室取消邀請", + "Remove from space": "從空間移除", + "Disinvite from space": "從空間取消邀請", + "Right-click message context menu": "右鍵點擊訊息情境選單" } From bca9caa98e4052fa9d76a59215740a3bd02e8fe7 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 19 Apr 2022 15:53:59 +0200 Subject: [PATCH 015/102] Settings toggle to disable Composer Markdown (#8358) --- res/css/views/elements/_SettingsFlag.scss | 6 + .../views/rooms/BasicMessageComposer.tsx | 20 ++- .../views/rooms/EditMessageComposer.tsx | 9 +- .../views/rooms/SendMessageComposer.tsx | 5 +- .../tabs/user/PreferencesUserSettingsTab.tsx | 1 + src/editor/deserialize.ts | 79 ++++++----- src/editor/serialize.ts | 15 ++- src/i18n/strings/en_EN.json | 2 + src/settings/Settings.tsx | 12 +- src/settings/SettingsStore.ts | 9 +- test/editor/deserialize-test.ts | 74 ++++++++++ test/editor/serialize-test.ts | 126 ++++++++++-------- 12 files changed, 260 insertions(+), 98 deletions(-) diff --git a/res/css/views/elements/_SettingsFlag.scss b/res/css/views/elements/_SettingsFlag.scss index 533487d98cf..c6f4cf6ec5c 100644 --- a/res/css/views/elements/_SettingsFlag.scss +++ b/res/css/views/elements/_SettingsFlag.scss @@ -41,4 +41,10 @@ limitations under the License. font-size: $font-12px; line-height: $font-15px; color: $secondary-content; + + // Support code/pre elements in settings flag descriptions + pre, code { + font-family: $monospace-font-family !important; + background-color: $rte-code-bg-color; + } } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index a5dcf038133..e93119643fb 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -103,6 +103,7 @@ interface IProps { } interface IState { + useMarkdown: boolean; showPillAvatar: boolean; query?: string; showVisualBell?: boolean; @@ -124,6 +125,7 @@ export default class BasicMessageEditor extends React.Component private lastCaret: DocumentOffset; private lastSelection: ReturnType; + private readonly useMarkdownHandle: string; private readonly emoticonSettingHandle: string; private readonly shouldShowPillAvatarSettingHandle: string; private readonly surroundWithHandle: string; @@ -133,10 +135,13 @@ export default class BasicMessageEditor extends React.Component super(props); this.state = { showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), + useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"), showVisualBell: false, }; + this.useMarkdownHandle = SettingsStore.watchSetting('MessageComposerInput.useMarkdown', null, + this.configureUseMarkdown); this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, this.configureEmoticonAutoReplace); this.configureEmoticonAutoReplace(); @@ -442,7 +447,7 @@ export default class BasicMessageEditor extends React.Component } } else if (!selection.isCollapsed && !isEmpty) { this.hasTextSelected = true; - if (this.formatBarRef.current) { + if (this.formatBarRef.current && this.state.useMarkdown) { const selectionRect = selection.getRangeAt(0).getBoundingClientRect(); this.formatBarRef.current.showAt(selectionRect); } @@ -630,6 +635,14 @@ export default class BasicMessageEditor extends React.Component this.setState({ completionIndex }); }; + private configureUseMarkdown = (): void => { + const useMarkdown = SettingsStore.getValue("MessageComposerInput.useMarkdown"); + this.setState({ useMarkdown }); + if (!useMarkdown && this.formatBarRef.current) { + this.formatBarRef.current.hide(); + } + }; + private configureEmoticonAutoReplace = (): void => { this.props.model.setTransformCallback(this.transform); }; @@ -654,6 +667,7 @@ export default class BasicMessageEditor extends React.Component this.editorRef.current.removeEventListener("input", this.onInput, true); this.editorRef.current.removeEventListener("compositionstart", this.onCompositionStart, true); this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true); + SettingsStore.unwatchSetting(this.useMarkdownHandle); SettingsStore.unwatchSetting(this.emoticonSettingHandle); SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle); SettingsStore.unwatchSetting(this.surroundWithHandle); @@ -694,6 +708,10 @@ export default class BasicMessageEditor extends React.Component } public onFormatAction = (action: Formatting): void => { + if (!this.state.useMarkdown) { + return; + } + const range: Range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); this.historyManager.ensureLastChangesPushed(this.props.model); diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index ce6d1b844e0..de1bdc9c85b 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -95,7 +95,10 @@ function createEditContent( body: `${plainPrefix} * ${body}`, }; - const formattedBody = htmlSerializeIfNeeded(model, { forceHTML: isReply }); + const formattedBody = htmlSerializeIfNeeded(model, { + forceHTML: isReply, + useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), + }); if (formattedBody) { newContent.format = "org.matrix.custom.html"; newContent.formatted_body = formattedBody; @@ -404,7 +407,9 @@ class EditMessageComposer extends React.Component { if (textPart.length) { - parts.push(...pc.plainWithEmoji(shouldEscape ? escape(textPart) : textPart)); + parts.push(...pc.plainWithEmoji(opts.shouldEscape ? escape(textPart) : textPart)); } // it's safe to never append @room after the last textPart // as split will report an empty string at the end if @@ -70,7 +70,7 @@ function parseAtRoomMentions(text: string, pc: PartCreator, shouldEscape = true) return parts; } -function parseLink(n: Node, pc: PartCreator): Part[] { +function parseLink(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { const { href } = n as HTMLAnchorElement; const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID @@ -81,18 +81,18 @@ function parseLink(n: Node, pc: PartCreator): Part[] { const children = Array.from(n.childNodes); if (href === n.textContent && children.every(c => c.nodeType === Node.TEXT_NODE)) { - return parseAtRoomMentions(n.textContent, pc); + return parseAtRoomMentions(n.textContent, pc, opts); } else { - return [pc.plain("["), ...parseChildren(n, pc), pc.plain(`](${href})`)]; + return [pc.plain("["), ...parseChildren(n, pc, opts), pc.plain(`](${href})`)]; } } -function parseImage(n: Node, pc: PartCreator): Part[] { +function parseImage(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { const { alt, src } = n as HTMLImageElement; return pc.plainWithEmoji(`![${escape(alt)}](${src})`); } -function parseCodeBlock(n: Node, pc: PartCreator): Part[] { +function parseCodeBlock(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { let language = ""; if (n.firstChild?.nodeName === "CODE") { for (const className of (n.firstChild as HTMLElement).classList) { @@ -117,10 +117,10 @@ function parseCodeBlock(n: Node, pc: PartCreator): Part[] { return parts; } -function parseHeader(n: Node, pc: PartCreator): Part[] { +function parseHeader(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { const depth = parseInt(n.nodeName.slice(1), 10); const prefix = pc.plain("#".repeat(depth) + " "); - return [prefix, ...parseChildren(n, pc)]; + return [prefix, ...parseChildren(n, pc, opts)]; } function checkIgnored(n) { @@ -144,10 +144,10 @@ function prefixLines(parts: Part[], prefix: string, pc: PartCreator) { } } -function parseChildren(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): Part[] { +function parseChildren(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (li: Node) => Part[]): Part[] { let prev; return Array.from(n.childNodes).flatMap(c => { - const parsed = parseNode(c, pc, mkListItem); + const parsed = parseNode(c, pc, opts, mkListItem); if (parsed.length && prev && (checkBlockNode(prev) || checkBlockNode(c))) { if (isListChild(c)) { // Use tighter spacing within lists @@ -161,12 +161,12 @@ function parseChildren(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part }); } -function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): Part[] { +function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (li: Node) => Part[]): Part[] { if (checkIgnored(n)) return []; switch (n.nodeType) { case Node.TEXT_NODE: - return parseAtRoomMentions(n.nodeValue, pc); + return parseAtRoomMentions(n.nodeValue, pc, opts); case Node.ELEMENT_NODE: switch (n.nodeName) { case "H1": @@ -175,43 +175,43 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): case "H4": case "H5": case "H6": - return parseHeader(n, pc); + return parseHeader(n, pc, opts); case "A": - return parseLink(n, pc); + return parseLink(n, pc, opts); case "IMG": - return parseImage(n, pc); + return parseImage(n, pc, opts); case "BR": return [pc.newline()]; case "HR": return [pc.plain("---")]; case "EM": - return [pc.plain("_"), ...parseChildren(n, pc), pc.plain("_")]; + return [pc.plain("_"), ...parseChildren(n, pc, opts), pc.plain("_")]; case "STRONG": - return [pc.plain("**"), ...parseChildren(n, pc), pc.plain("**")]; + return [pc.plain("**"), ...parseChildren(n, pc, opts), pc.plain("**")]; case "DEL": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "SUB": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "SUP": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "U": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "PRE": - return parseCodeBlock(n, pc); + return parseCodeBlock(n, pc, opts); case "CODE": { // Escape backticks by using multiple backticks for the fence if necessary const fence = "`".repeat(longestBacktickSequence(n.textContent) + 1); return pc.plainWithEmoji(`${fence}${n.textContent}${fence}`); } case "BLOCKQUOTE": { - const parts = parseChildren(n, pc); + const parts = parseChildren(n, pc, opts); prefixLines(parts, "> ", pc); return parts; } case "LI": - return mkListItem?.(n) ?? parseChildren(n, pc); + return mkListItem?.(n) ?? parseChildren(n, pc, opts); case "UL": { - const parts = parseChildren(n, pc, li => [pc.plain("- "), ...parseChildren(li, pc)]); + const parts = parseChildren(n, pc, opts, li => [pc.plain("- "), ...parseChildren(li, pc, opts)]); if (isListChild(n)) { prefixLines(parts, " ", pc); } @@ -219,8 +219,8 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): } case "OL": { let counter = (n as HTMLOListElement).start ?? 1; - const parts = parseChildren(n, pc, li => { - const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc)]; + const parts = parseChildren(n, pc, opts, li => { + const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc, opts)]; counter++; return parts; }); @@ -247,15 +247,20 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): } } - return parseChildren(n, pc); + return parseChildren(n, pc, opts); } -function parseHtmlMessage(html: string, pc: PartCreator, isQuotedMessage: boolean): Part[] { +interface IParseOptions { + isQuotedMessage?: boolean; + shouldEscape?: boolean; +} + +function parseHtmlMessage(html: string, pc: PartCreator, opts: IParseOptions): Part[] { // no nodes from parsing here should be inserted in the document, // as scripts in event handlers, etc would be executed then. // we're only taking text, so that is fine - const parts = parseNode(new DOMParser().parseFromString(html, "text/html").body, pc); - if (isQuotedMessage) { + const parts = parseNode(new DOMParser().parseFromString(html, "text/html").body, pc, opts); + if (opts.isQuotedMessage) { prefixLines(parts, "> ", pc); } return parts; @@ -264,14 +269,14 @@ function parseHtmlMessage(html: string, pc: PartCreator, isQuotedMessage: boolea export function parsePlainTextMessage( body: string, pc: PartCreator, - opts: { isQuotedMessage?: boolean, shouldEscape?: boolean }, + opts: IParseOptions, ): Part[] { const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n return lines.reduce((parts, line, i) => { if (opts.isQuotedMessage) { parts.push(pc.plain("> ")); } - parts.push(...parseAtRoomMentions(line, pc, opts.shouldEscape)); + parts.push(...parseAtRoomMentions(line, pc, opts)); const isLast = i === lines.length - 1; if (!isLast) { parts.push(pc.newline()); @@ -280,19 +285,19 @@ export function parsePlainTextMessage( }, [] as Part[]); } -export function parseEvent(event: MatrixEvent, pc: PartCreator, { isQuotedMessage = false } = {}) { +export function parseEvent(event: MatrixEvent, pc: PartCreator, opts: IParseOptions = { shouldEscape: true }) { const content = event.getContent(); let parts: Part[]; const isEmote = content.msgtype === "m.emote"; let isRainbow = false; if (content.format === "org.matrix.custom.html") { - parts = parseHtmlMessage(content.formatted_body || "", pc, isQuotedMessage); + parts = parseHtmlMessage(content.formatted_body || "", pc, opts); if (content.body && content.formatted_body && textToHtmlRainbow(content.body) === content.formatted_body) { isRainbow = true; } } else { - parts = parsePlainTextMessage(content.body || "", pc, { isQuotedMessage }); + parts = parsePlainTextMessage(content.body || "", pc, opts); } if (isEmote && isRainbow) { diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 8e0d3d66db9..7c4d62e9ab5 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -17,6 +17,7 @@ limitations under the License. import { AllHtmlEntities } from 'html-entities'; import cheerio from 'cheerio'; +import escapeHtml from "escape-html"; import Markdown from '../Markdown'; import { makeGenericPermalink } from "../utils/permalinks/Permalinks"; @@ -48,7 +49,19 @@ export function mdSerialize(model: EditorModel): string { }, ""); } -export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string { +interface ISerializeOpts { + forceHTML?: boolean; + useMarkdown?: boolean; +} + +export function htmlSerializeIfNeeded( + model: EditorModel, + { forceHTML = false, useMarkdown = true }: ISerializeOpts = {}, +): string { + if (!useMarkdown) { + return escapeHtml(textSerialize(model)).replace(/\n/g, '
    '); + } + let md = mdSerialize(model); // copy of raw input to remove unwanted math later const orig = md; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 46632c34499..54fc8a8c8fc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -932,6 +932,8 @@ "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", "Surround selected text when typing special characters": "Surround selected text when typing special characters", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", + "Enable Markdown": "Enable Markdown", + "Start messages with /plain to send without markdown and /md to send with.": "Start messages with /plain to send without markdown and /md to send with.", "Mirror local video feed": "Mirror local video feed", "Match system theme": "Match system theme", "Use a system font": "Use a system font", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index fa773080be2..d9bd0817a4f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -133,7 +133,7 @@ export interface IBaseSetting { }; // Optional description which will be shown as microCopy under SettingsFlags - description?: string; + description?: string | (() => ReactNode); // The supported levels are required. Preferably, use the preset arrays // at the top of this file to define this rather than a custom array. @@ -611,6 +611,16 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td('Automatically replace plain text Emoji'), default: false, }, + "MessageComposerInput.useMarkdown": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Enable Markdown'), + description: () => _t( + "Start messages with /plain to send without markdown and /md to send with.", + {}, + { code: (sub) => { sub } }, + ), + default: true, + }, "VideoView.flipVideoHorizontally": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Mirror local video feed'), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 95ea0e6993e..19a419afb7f 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -16,6 +16,7 @@ limitations under the License. */ import { logger } from "matrix-js-sdk/src/logger"; +import { ReactNode } from "react"; import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler"; import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler"; @@ -257,9 +258,11 @@ export default class SettingsStore { * @param {string} settingName The setting to look up. * @return {String} The description for the setting, or null if not found. */ - public static getDescription(settingName: string) { - if (!SETTINGS[settingName]?.description) return null; - return _t(SETTINGS[settingName].description); + public static getDescription(settingName: string): string | ReactNode { + const description = SETTINGS[settingName]?.description; + if (!description) return null; + if (typeof description !== 'string') return description(); + return _t(description); } /** diff --git a/test/editor/deserialize-test.ts b/test/editor/deserialize-test.ts index 86594f78dfe..47ab6cb2f27 100644 --- a/test/editor/deserialize-test.ts +++ b/test/editor/deserialize-test.ts @@ -331,4 +331,78 @@ describe('editor/deserialize', function() { expect(parts).toMatchSnapshot(); }); }); + describe('plaintext messages', function() { + it('turns html tags back into markdown', function() { + const html = "bold and emphasized text this!"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "**bold** and _emphasized_ text [this](http://example.com/)!", + }); + }); + it('keeps backticks unescaped', () => { + const html = "this → ` is a backtick and here are 3 of them:\n```"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "this → ` is a backtick and here are 3 of them:\n```", + }); + }); + it('keeps backticks outside of code blocks', () => { + const html = "some `backticks`"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "some `backticks`", + }); + }); + it('keeps backslashes', () => { + const html = "C:\\My Documents"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "C:\\My Documents", + }); + }); + it('keeps asterisks', () => { + const html = "*hello*"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "*hello*", + }); + }); + it('keeps underscores', () => { + const html = "__emphasis__"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "__emphasis__", + }); + }); + it('keeps square brackets', () => { + const html = "[not an actual link](https://example.org)"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "[not an actual link](https://example.org)", + }); + }); + it('escapes angle brackets', () => { + const html = "> <del>no formatting here</del>"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "> no formatting here", + }); + }); + }); }); diff --git a/test/editor/serialize-test.ts b/test/editor/serialize-test.ts index 40f95e03773..d9482859015 100644 --- a/test/editor/serialize-test.ts +++ b/test/editor/serialize-test.ts @@ -19,58 +19,80 @@ import { htmlSerializeIfNeeded } from "../../src/editor/serialize"; import { createPartCreator } from "./mock"; describe('editor/serialize', function() { - it('user pill turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Alice"); + describe('with markdown', function() { + it('user pill turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Alice"); + }); + it('room pill turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.roomPill("#room:hs.tld")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("#room:hs.tld"); + }); + it('@room pill turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.atRoomPill("@room")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBeFalsy(); + }); + it('any markdown turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("*hello* world")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("hello world"); + }); + it('displaynames ending in a backslash work', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Displayname\\", "@user:server")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Displayname\\"); + }); + it('displaynames containing an opening square bracket work', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Displayname[[", "@user:server")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Displayname[["); + }); + it('displaynames containing a closing square bracket work', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Displayname]", "@user:server")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Displayname]"); + }); + it('escaped markdown should not retain backslashes', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world')], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('*hello* world'); + }); + it('escaped markdown should convert HTML entities', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('*hello* world < hey world!'); + }); }); - it('room pill turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.roomPill("#room:hs.tld")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("#room:hs.tld"); - }); - it('@room pill turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.atRoomPill("@room")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBeFalsy(); - }); - it('any markdown turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.plain("*hello* world")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("hello world"); - }); - it('displaynames ending in a backslash work', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Displayname\\", "@user:server")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Displayname\\"); - }); - it('displaynames containing an opening square bracket work', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Displayname[[", "@user:server")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Displayname[["); - }); - it('displaynames containing a closing square bracket work', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Displayname]", "@user:server")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Displayname]"); - }); - it('escaped markdown should not retain backslashes', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.plain('\\*hello\\* world')], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe('*hello* world'); - }); - it('escaped markdown should convert HTML entities', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe('*hello* world < hey world!'); + describe('with plaintext', function() { + it('markdown remains plaintext', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("*hello* world")], pc); + const html = htmlSerializeIfNeeded(model, { useMarkdown: false }); + expect(html).toBe("*hello* world"); + }); + it('markdown should retain backslashes', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world')], pc); + const html = htmlSerializeIfNeeded(model, { useMarkdown: false }); + expect(html).toBe('\\*hello\\* world'); + }); + it('markdown should convert HTML entities', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')], pc); + const html = htmlSerializeIfNeeded(model, { useMarkdown: false }); + expect(html).toBe('\\*hello\\* world < hey world!'); + }); }); }); From 1da1460baf68d3b50b7b202741a52a71a69d123f Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 19 Apr 2022 14:54:05 +0100 Subject: [PATCH 016/102] Upgrade matrix-js-sdk to 17.1.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 987c12eb06d..e911e1c4955 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#daad3faed54f0b1f1e026a7498b4653e4d01cd90", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "17.1.0-rc.1", "matrix-widget-api": "^0.1.0-beta.18", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index d5f058af71f..7e021ae9857 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6651,9 +6651,10 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "17.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b58d09aa9a7a3578c43185becb41ab0b17ce0f98" +matrix-js-sdk@17.1.0-rc.1: + version "17.1.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-17.1.0-rc.1.tgz#e96582e6c35d7c1e77c54b397fa6748fbdabe031" + integrity sha512-iqMS+Sxj6Y+cGS5oV7hnoQp0v58O+4HVjGkxfrJ0mskCJ8xsNQU3M4V8NA8e7atXHzhgqcfbB8RkcHe+WGE1fw== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From ff9a563338f2221d57afce2335e5aa0d5a2227aa Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 19 Apr 2022 14:55:24 +0100 Subject: [PATCH 017/102] Prepare changelog for v3.43.0-rc.1 --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0126f1f369f..5a5a1713048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +Changes in [3.43.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.43.0-rc.1) (2022-04-19) +=============================================================================================================== + +## ✨ Features + * Improve performance of switching to rooms with lots of servers and ACLs ([\#8347](https://github.com/matrix-org/matrix-react-sdk/pull/8347)). + * Avoid a reflow when setting caret position on an empty composer ([\#8348](https://github.com/matrix-org/matrix-react-sdk/pull/8348)). + * Add message right-click context menu as a labs feature ([\#5672](https://github.com/matrix-org/matrix-react-sdk/pull/5672)). Contributed by @SimonBrandner. + * Live location sharing - basic maximised beacon map ([\#8310](https://github.com/matrix-org/matrix-react-sdk/pull/8310)). + * Live location sharing - render users own beacons in timeline ([\#8296](https://github.com/matrix-org/matrix-react-sdk/pull/8296)). + * Improve Threads beta around degraded mode ([\#8318](https://github.com/matrix-org/matrix-react-sdk/pull/8318)). + * Live location sharing - beacon in timeline happy path ([\#8285](https://github.com/matrix-org/matrix-react-sdk/pull/8285)). + * Add copy button to View Source screen ([\#8278](https://github.com/matrix-org/matrix-react-sdk/pull/8278)). Fixes vector-im/element-web#21482. Contributed by @olivialivia. + * Add heart effect ([\#6188](https://github.com/matrix-org/matrix-react-sdk/pull/6188)). Contributed by @CicadaCinema. + * Update new room icon ([\#8239](https://github.com/matrix-org/matrix-react-sdk/pull/8239)). + +## 🐛 Bug Fixes + * Fix: "Code formatting button does not escape backticks" ([\#8181](https://github.com/matrix-org/matrix-react-sdk/pull/8181)). Contributed by @yaya-usman. + * Fix beta indicator dot causing excessive CPU usage ([\#8340](https://github.com/matrix-org/matrix-react-sdk/pull/8340)). Fixes vector-im/element-web#21793. + * Fix overlapping timestamps on empty messages ([\#8205](https://github.com/matrix-org/matrix-react-sdk/pull/8205)). Fixes vector-im/element-web#21381. Contributed by @goelesha. + * Fix power selector not showing up in user info when state_default undefined ([\#8297](https://github.com/matrix-org/matrix-react-sdk/pull/8297)). Fixes vector-im/element-web#21669. + * Avoid looking up settings during timeline rendering ([\#8313](https://github.com/matrix-org/matrix-react-sdk/pull/8313)). Fixes vector-im/element-web#21740. + * Fix a soft crash with video rooms ([\#8333](https://github.com/matrix-org/matrix-react-sdk/pull/8333)). + * Fixes call tiles overflow ([\#8096](https://github.com/matrix-org/matrix-react-sdk/pull/8096)). Fixes vector-im/element-web#20254. Contributed by @luixxiul. + * Fix a bug with emoji autocomplete sorting where adding the final ":" would cause the emoji with the typed shortcode to no longer be at the top of the autocomplete list. ([\#8086](https://github.com/matrix-org/matrix-react-sdk/pull/8086)). Fixes vector-im/element-web#19302. Contributed by @commonlawfeature. + * Fix image preview sizing for edge cases ([\#8322](https://github.com/matrix-org/matrix-react-sdk/pull/8322)). Fixes vector-im/element-web#20088. + * Refactor SecurityRoomSettingsTab and remove unused state ([\#8306](https://github.com/matrix-org/matrix-react-sdk/pull/8306)). Fixes matrix-org/element-web-rageshakes#12002. + * Don't show the prompt to enable desktop notifications immediately after registration ([\#8274](https://github.com/matrix-org/matrix-react-sdk/pull/8274)). + * Stop tracking threads if threads support is disabled ([\#8308](https://github.com/matrix-org/matrix-react-sdk/pull/8308)). Fixes vector-im/element-web#21766. + * Fix some issues with threads rendering ([\#8305](https://github.com/matrix-org/matrix-react-sdk/pull/8305)). Fixes vector-im/element-web#21670. + * Fix threads rendering issue in Safari ([\#8298](https://github.com/matrix-org/matrix-react-sdk/pull/8298)). Fixes vector-im/element-web#21757. + * Fix space panel width change on hovering over space item ([\#8299](https://github.com/matrix-org/matrix-react-sdk/pull/8299)). Fixes vector-im/element-web#19891. + * Hide the reply in thread button in deployments where beta is forcibly disabled ([\#8294](https://github.com/matrix-org/matrix-react-sdk/pull/8294)). Fixes vector-im/element-web#21753. + * Prevent soft crash around room list header context menu when space changes ([\#8289](https://github.com/matrix-org/matrix-react-sdk/pull/8289)). Fixes matrix-org/element-web-rageshakes#11416, matrix-org/element-web-rageshakes#11692, matrix-org/element-web-rageshakes#11739, matrix-org/element-web-rageshakes#11772, matrix-org/element-web-rageshakes#11891 matrix-org/element-web-rageshakes#11858 and matrix-org/element-web-rageshakes#11456. + * When selecting reply in thread on a thread response open existing thread ([\#8291](https://github.com/matrix-org/matrix-react-sdk/pull/8291)). Fixes vector-im/element-web#21743. + * Handle thread bundled relationships coming from the server via MSC3666 ([\#8292](https://github.com/matrix-org/matrix-react-sdk/pull/8292)). Fixes vector-im/element-web#21450. + * Fix: Avatar preview does not update when same file is selected repeatedly ([\#8288](https://github.com/matrix-org/matrix-react-sdk/pull/8288)). Fixes vector-im/element-web#20098. + * Fix a bug where user gets a warning when changing powerlevel from **Admin** to **custom level (100)** ([\#8248](https://github.com/matrix-org/matrix-react-sdk/pull/8248)). Fixes vector-im/element-web#21682. Contributed by @Jumeb. + * Use a consistent alignment for all text items in a list ([\#8276](https://github.com/matrix-org/matrix-react-sdk/pull/8276)). Fixes vector-im/element-web#21731. Contributed by @luixxiul. + * Fixes button labels being collapsed per a character in CJK languages ([\#8212](https://github.com/matrix-org/matrix-react-sdk/pull/8212)). Fixes vector-im/element-web#21287. Contributed by @luixxiul. + * Fix: Remove jittery timeline scrolling after jumping to an event ([\#8263](https://github.com/matrix-org/matrix-react-sdk/pull/8263)). + * Fix regression of edits showing up in the timeline with hidden events shown ([\#8260](https://github.com/matrix-org/matrix-react-sdk/pull/8260)). Fixes vector-im/element-web#21694. + * Fix reporting events not working ([\#8257](https://github.com/matrix-org/matrix-react-sdk/pull/8257)). Fixes vector-im/element-web#21713. + * Make Jitsi widgets in video rooms immutable ([\#8244](https://github.com/matrix-org/matrix-react-sdk/pull/8244)). Fixes vector-im/element-web#21647. + * Fix: Ensure links to events scroll the correct events into view ([\#8250](https://github.com/matrix-org/matrix-react-sdk/pull/8250)). Fixes vector-im/element-web#19934. + Changes in [3.42.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.42.3) (2022-04-12) ===================================================================================================== From db89816db909cfb125dcea8aae36f526ebb6b61f Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 19 Apr 2022 14:55:24 +0100 Subject: [PATCH 018/102] v3.43.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e911e1c4955..b80d9d9975d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.42.3", + "version": "3.43.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -22,7 +22,7 @@ "README.md", "package.json" ], - "main": "./src/index.ts", + "main": "./lib/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -236,5 +236,6 @@ "text", "json" ] - } + }, + "typings": "./lib/index.d.ts" } From f099fdb906411421b83cdcb116f3249bfbe44f79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 19 Apr 2022 17:21:31 +0100 Subject: [PATCH 019/102] Increase delay to stabilise async tests (#8357) --- test/end-to-end-tests/src/usecases/threads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/src/usecases/threads.ts b/test/end-to-end-tests/src/usecases/threads.ts index 49870815a6d..4baaa49ce94 100644 --- a/test/end-to-end-tests/src/usecases/threads.ts +++ b/test/end-to-end-tests/src/usecases/threads.ts @@ -92,7 +92,7 @@ export async function redactThreadMessage(session: ElementSession): Promise Date: Tue, 19 Apr 2022 17:21:40 +0100 Subject: [PATCH 020/102] Fix issue with ServerInfo crashing the modal (#8364) --- src/components/views/dialogs/devtools/ServerInfo.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/devtools/ServerInfo.tsx b/src/components/views/dialogs/devtools/ServerInfo.tsx index ff18d836e5c..23b6528eacc 100644 --- a/src/components/views/dialogs/devtools/ServerInfo.tsx +++ b/src/components/views/dialogs/devtools/ServerInfo.tsx @@ -74,13 +74,13 @@ const ServerInfo = ({ onBack }: IDevtoolsProps) => { }

    { _t("Client Versions") }

    - { capabilities !== FAILED_TO_LOAD + { clientVersions !== FAILED_TO_LOAD ? :
    { _t("Failed to load.") }
    }

    { _t("Server Versions") }

    - { capabilities !== FAILED_TO_LOAD + { serverVersions !== FAILED_TO_LOAD ? :
    { _t("Failed to load.") }
    } From 70cdd57a5c61372b9e053edfa57e7a9aed06937e Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 19 Apr 2022 18:47:19 +0200 Subject: [PATCH 021/102] Live location sharing: move test utils into utils (#8365) * move makeRoomWithState events to test utils Signed-off-by: Kerry Archibald * move beacon test helpers into utils Signed-off-by: Kerry Archibald * remove file Signed-off-by: Kerry Archibald * more types Signed-off-by: Kerry Archibald --- .../views/beacon/BeaconMarker-test.tsx | 24 +++++----- .../views/beacon/BeaconViewDialog-test.tsx | 15 +++--- .../views/messages/MBeaconBody-test.tsx | 47 ++++++++----------- test/test-utils/beacon.ts | 27 ++++++++++- test/test-utils/room.ts | 18 +++++++ 5 files changed, 84 insertions(+), 47 deletions(-) diff --git a/test/components/views/beacon/BeaconMarker-test.tsx b/test/components/views/beacon/BeaconMarker-test.tsx index 5b730ff4387..efc9c7d22c5 100644 --- a/test/components/views/beacon/BeaconMarker-test.tsx +++ b/test/components/views/beacon/BeaconMarker-test.tsx @@ -22,12 +22,18 @@ import { Beacon, Room, RoomMember, + MatrixEvent, getBeaconInfoIdentifier, } from 'matrix-js-sdk/src/matrix'; import BeaconMarker from '../../../../src/components/views/beacon/BeaconMarker'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; -import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent } from '../../../test-utils'; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makeRoomWithStateEvents, +} from '../../../test-utils'; import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; describe('', () => { @@ -53,13 +59,9 @@ describe('', () => { // make fresh rooms every time // as we update room state - const makeRoomWithStateEvents = (stateEvents = []): Room => { - const room1 = new Room(roomId, mockClient, aliceId); - - room1.currentState.setStateEvents(stateEvents); + const setupRoom = (stateEvents: MatrixEvent[] = []): Room => { + const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient }); jest.spyOn(room1, 'getMember').mockReturnValue(aliceMember); - mockClient.getRoom.mockReturnValue(room1); - return room1; }; @@ -97,21 +99,21 @@ describe('', () => { }); it('renders nothing when beacon is not live', () => { - const room = makeRoomWithStateEvents([notLiveEvent]); + const room = setupRoom([notLiveEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(notLiveEvent)); const component = getComponent({ beacon }); expect(component.html()).toBe(null); }); it('renders nothing when beacon has no location', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); const component = getComponent({ beacon }); expect(component.html()).toBe(null); }); it('renders marker when beacon has location', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent({ beacon }); @@ -119,7 +121,7 @@ describe('', () => { }); it('updates with new locations', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent({ beacon }); diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx index b3573de0f99..197c0b7e7f5 100644 --- a/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -19,6 +19,7 @@ import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { MatrixClient, + MatrixEvent, Room, RoomMember, getBeaconInfoIdentifier, @@ -30,6 +31,7 @@ import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent, + makeRoomWithStateEvents, } from '../../../test-utils'; import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; @@ -55,12 +57,9 @@ describe('', () => { // make fresh rooms every time // as we update room state - const makeRoomWithStateEvents = (stateEvents = []): Room => { - const room1 = new Room(roomId, mockClient, aliceId); - - room1.currentState.setStateEvents(stateEvents); + const setupRoom = (stateEvents: MatrixEvent[] = []): Room => { + const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient }); jest.spyOn(room1, 'getMember').mockReturnValue(aliceMember); - mockClient.getRoom.mockReturnValue(room1); return room1; }; @@ -85,7 +84,7 @@ describe('', () => { mount(); it('renders a map with markers', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent(); @@ -97,7 +96,7 @@ describe('', () => { }); it('updates markers on changes to beacons', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent(); @@ -122,7 +121,7 @@ describe('', () => { it('renders a fallback when no live beacons remain', () => { const onFinished = jest.fn(); - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent({ onFinished }); diff --git a/test/components/views/messages/MBeaconBody-test.tsx b/test/components/views/messages/MBeaconBody-test.tsx index b37bf65bbde..5afbb05c78b 100644 --- a/test/components/views/messages/MBeaconBody-test.tsx +++ b/test/components/views/messages/MBeaconBody-test.tsx @@ -20,12 +20,16 @@ import { act } from 'react-dom/test-utils'; import maplibregl from 'maplibre-gl'; import { BeaconEvent, - Room, getBeaconInfoIdentifier, } from 'matrix-js-sdk/src/matrix'; import MBeaconBody from '../../../../src/components/views/messages/MBeaconBody'; -import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent } from '../../../test-utils'; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makeRoomWithStateEvents, +} from '../../../test-utils'; import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; import { MediaEventHelper } from '../../../../src/utils/MediaEventHelper'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; @@ -51,17 +55,6 @@ describe('', () => { getRoom: jest.fn(), }); - // make fresh rooms every time - // as we update room state - const makeRoomWithStateEvents = (stateEvents = []): Room => { - const room1 = new Room(roomId, mockClient, aliceId); - - room1.currentState.setStateEvents(stateEvents); - mockClient.getRoom.mockReturnValue(room1); - - return room1; - }; - const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, @@ -96,7 +89,7 @@ describe('', () => { { isLive: false }, '$alice-room1-1', ); - makeRoomWithStateEvents([beaconInfoEvent]); + makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); expect(component.text()).toEqual("Live location ended"); }); @@ -108,7 +101,7 @@ describe('', () => { { isLive: true, timestamp: now - 600000, timeout: 500 }, '$alice-room1-1', ); - makeRoomWithStateEvents([beaconInfoEvent]); + makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); expect(component.text()).toEqual("Live location ended"); }); @@ -120,7 +113,7 @@ describe('', () => { { isLive: true, timestamp: now - 600000, timeout: 500 }, '$alice-room1-1', ); - makeRoomWithStateEvents([beaconInfoEvent]); + makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); act(() => { component.find('.mx_MBeaconBody_map').simulate('click'); @@ -145,7 +138,7 @@ describe('', () => { '$alice-room1-2', ); - makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2]); + makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo1 }); // beacon1 has been superceded by beacon2 @@ -168,7 +161,7 @@ describe('', () => { '$alice-room1-2', ); - const room = makeRoomWithStateEvents([aliceBeaconInfo1]); + const room = makeRoomWithStateEvents([aliceBeaconInfo1], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo1 }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo1)); @@ -193,7 +186,7 @@ describe('', () => { '$alice-room1-1', ); - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -226,14 +219,14 @@ describe('', () => { ); it('renders a live beacon without a location correctly', () => { - makeRoomWithStateEvents([aliceBeaconInfo]); + makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); expect(component.text()).toEqual("Loading live location..."); }); it('does nothing on click when a beacon has no location', () => { - makeRoomWithStateEvents([aliceBeaconInfo]); + makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); act(() => { @@ -244,7 +237,7 @@ describe('', () => { }); it('renders a live beacon with a location correctly', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -253,7 +246,7 @@ describe('', () => { }); it('opens maximised map view on click when beacon has a live location', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -267,7 +260,7 @@ describe('', () => { }); it('does nothing on click when a beacon has no location', () => { - makeRoomWithStateEvents([aliceBeaconInfo]); + makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); act(() => { @@ -278,7 +271,7 @@ describe('', () => { }); it('renders a live beacon with a location correctly', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -287,7 +280,7 @@ describe('', () => { }); it('opens maximised map view on click when beacon has a live location', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -301,7 +294,7 @@ describe('', () => { }); it('updates latest location', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); diff --git a/test/test-utils/beacon.ts b/test/test-utils/beacon.ts index 19764a2600b..3eee57b39f5 100644 --- a/test/test-utils/beacon.ts +++ b/test/test-utils/beacon.ts @@ -16,11 +16,17 @@ limitations under the License. import { MockedObject } from "jest-mock"; import { makeBeaconInfoContent, makeBeaconContent } from "matrix-js-sdk/src/content-helpers"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { + MatrixClient, + MatrixEvent, + Beacon, + getBeaconInfoIdentifier, +} from "matrix-js-sdk/src/matrix"; import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { LocationAssetType } from "matrix-js-sdk/src/@types/location"; import { getMockGeolocationPositionError } from "./location"; +import { makeRoomWithStateEvents } from "./room"; type InfoContentProps = { timeout: number; @@ -182,3 +188,22 @@ export const watchPositionMockImplementation = (delays: number[], errorCodes: nu }); }; }; + +/** + * Creates a room with beacon events + * sets given locations on beacons + * returns beacons + */ +export const makeRoomWithBeacons = ( + roomId: string, + mockClient: MockedObject, + beaconInfoEvents: MatrixEvent[], + locationEvents?: MatrixEvent[], +): Beacon[] => { + const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient }); + const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event))); + if (locationEvents) { + beacons.forEach(beacon => beacon.addLocations(locationEvents)); + } + return beacons; +}; diff --git a/test/test-utils/room.ts b/test/test-utils/room.ts index 022f13e6c1a..b9224e38710 100644 --- a/test/test-utils/room.ts +++ b/test/test-utils/room.ts @@ -14,8 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MockedObject } from "jest-mock"; import { + MatrixClient, + MatrixEvent, EventType, + Room, } from "matrix-js-sdk/src/matrix"; import { mkEvent } from "./test-utils"; @@ -32,3 +36,17 @@ export const makeMembershipEvent = ( ts: Date.now(), }); +/** + * Creates a room + * sets state events on the room + * Sets client getRoom to return room + * returns room + */ +export const makeRoomWithStateEvents = ( + stateEvents: MatrixEvent[] = [], + { roomId, mockClient }: { roomId: string, mockClient: MockedObject}): Room => { + const room1 = new Room(roomId, mockClient, '@user:server.org'); + room1.currentState.setStateEvents(stateEvents); + mockClient.getRoom.mockReturnValue(room1); + return room1; +}; From 2fcf7875788310168a0151db75b110c18d0448fa Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 20 Apr 2022 07:41:27 +0000 Subject: [PATCH 022/102] Use -webkit-line-clamp for the room header topic overflow (#8367) --- res/css/views/rooms/_RoomHeader.scss | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 3eabf5e57a3..7d25ade6bac 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -141,17 +141,24 @@ limitations under the License. } .mx_RoomHeader_topic { + $lineHeight: $font-16px; + $lines: 2; + flex: 1; color: $roomtopic-color; font-weight: 400; font-size: $font-13px; + line-height: $lineHeight; + max-height: calc($lineHeight * $lines); + border-bottom: 1px solid transparent; + // to align baseline of topic with room name margin: 4px 7px 0; + overflow: hidden; - text-overflow: ellipsis; - border-bottom: 1px solid transparent; - line-height: 1.2em; - max-height: 2.4em; + -webkit-line-clamp: $lines; // See: https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp + -webkit-box-orient: vertical; + display: -webkit-box; } .mx_RoomHeader_avatar { From a471742e972ab2d3e92f59b26ef891523acbd866 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 20 Apr 2022 07:48:38 +0000 Subject: [PATCH 023/102] Fix UnknownBody error message unalignment (#8346) --- src/components/views/messages/UnknownBody.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/UnknownBody.tsx b/src/components/views/messages/UnknownBody.tsx index d9e70ff241f..cd1f06a788b 100644 --- a/src/components/views/messages/UnknownBody.tsx +++ b/src/components/views/messages/UnknownBody.tsx @@ -23,12 +23,12 @@ interface IProps { children?: React.ReactNode; } -export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject) => { +export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject) => { const text = mxEvent.getContent().body; return ( - +
    { text } { children } - +
    ); }); From e45cd39906dec0f6d4d0c37b7db3440eaea52421 Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 20 Apr 2022 10:14:24 +0200 Subject: [PATCH 024/102] Live location sharing: maximised view sidebar container (#8360) * add h4 Signed-off-by: Kerry Archibald * add mixin to clear list style Signed-off-by: Kerry Archibald * add basic sidebar container Signed-off-by: Kerry Archibald * open list view button on beaconviewdialog Signed-off-by: Kerry Archibald * update tests for new utils Signed-off-by: Kerry Archibald --- res/css/_common.scss | 6 ++ res/css/_components.scss | 1 + .../views/beacon/_BeaconViewDialog.scss | 11 +++- .../views/beacon/_DialogSidebar.scss | 57 +++++++++++++++++++ res/css/views/typography/_Heading.scss | 8 +++ .../views/beacon/BeaconViewDialog.tsx | 18 +++++- src/components/views/beacon/DialogSidebar.tsx | 50 ++++++++++++++++ src/components/views/typography/Heading.tsx | 2 +- src/i18n/strings/en_EN.json | 3 + .../views/beacon/BeaconViewDialog-test.tsx | 39 +++++++++++++ .../views/beacon/DialogSidebar-test.tsx | 47 +++++++++++++++ .../__snapshots__/DialogSidebar-test.tsx.snap | 53 +++++++++++++++++ .../views/typography/Heading-test.tsx | 4 ++ .../__snapshots__/Heading-test.tsx.snap | 11 ++++ 14 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 res/css/components/views/beacon/_DialogSidebar.scss create mode 100644 src/components/views/beacon/DialogSidebar.tsx create mode 100644 test/components/views/beacon/DialogSidebar-test.tsx create mode 100644 test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap diff --git a/res/css/_common.scss b/res/css/_common.scss index 5c6349c2204..b5d873194dd 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -692,3 +692,9 @@ legend { } } } + +@define-mixin ListResetDefault { + list-style: none; + padding: 0; + margin: 0; +} diff --git a/res/css/_components.scss b/res/css/_components.scss index 8b5dc63a0e6..aa92057e633 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -6,6 +6,7 @@ @import "./_spacing.scss"; @import "./components/views/beacon/_BeaconStatus.scss"; @import "./components/views/beacon/_BeaconViewDialog.scss"; +@import "./components/views/beacon/_DialogSidebar.scss"; @import "./components/views/beacon/_LeftPanelLiveShareWarning.scss"; @import "./components/views/beacon/_LiveTimeRemaining.scss"; @import "./components/views/beacon/_OwnBeaconStatus.scss"; diff --git a/res/css/components/views/beacon/_BeaconViewDialog.scss b/res/css/components/views/beacon/_BeaconViewDialog.scss index dc4d089bfe5..6ad1a2a6139 100644 --- a/res/css/components/views/beacon/_BeaconViewDialog.scss +++ b/res/css/components/views/beacon/_BeaconViewDialog.scss @@ -29,6 +29,9 @@ limitations under the License. height: calc(80vh - 0.5px); overflow: hidden; + // sidebar is absolutely positioned inside + position: relative; + .mx_Dialog_header { margin: 0px; padding: 0px; @@ -40,7 +43,7 @@ limitations under the License. .mx_Dialog_cancelButton { z-index: 4010; - position: absolute; + position: fixed; right: 5vw; top: 5vh; width: 20px; @@ -77,3 +80,9 @@ limitations under the License. color: $secondary-content; margin-bottom: $spacing-16; } + +.mx_BeaconViewDialog_viewListButton { + position: absolute; + top: $spacing-24; + left: $spacing-24; +} diff --git a/res/css/components/views/beacon/_DialogSidebar.scss b/res/css/components/views/beacon/_DialogSidebar.scss new file mode 100644 index 00000000000..02d0e82cc33 --- /dev/null +++ b/res/css/components/views/beacon/_DialogSidebar.scss @@ -0,0 +1,57 @@ +/* +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. +*/ + +.mx_DialogSidebar { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 265px; + + box-sizing: border-box; + padding: $spacing-16; + + background-color: $background; + box-shadow: 0px 4px 4px $menu-box-shadow-color; +} + +.mx_DialogSidebar_header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + flex: 0; + margin-bottom: $spacing-16; + + color: $primary-content; +} + +.mx_DialogSidebar_closeButton { + @mixin ButtonResetDefault; +} + +.mx_DialogSidebar_closeButtonIcon { + color: $tertiary-content; + height: 12px; +} + +.mx_DialogSidebar_list { + @mixin ListResetDefault; + flex: 1 1 0; + width: 100%; + overflow: auto; +} diff --git a/res/css/views/typography/_Heading.scss b/res/css/views/typography/_Heading.scss index 9b7ddeaef3f..84a008c18f8 100644 --- a/res/css/views/typography/_Heading.scss +++ b/res/css/views/typography/_Heading.scss @@ -37,3 +37,11 @@ limitations under the License. margin-inline: unset; margin-block: unset; } + +.mx_Heading_h4 { + font-size: $font-15px; + font-weight: $font-semi-bold; + line-height: $font-20px; + margin-inline: unset; + margin-block: unset; +} diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 12f26a0a54f..76b9b75e3e4 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { Beacon, @@ -22,6 +22,7 @@ import { } from 'matrix-js-sdk/src/matrix'; import maplibregl from 'maplibre-gl'; +import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg'; import { useLiveBeacons } from '../../../utils/beacon/useLiveBeacons'; import MatrixClientContext from '../../../contexts/MatrixClientContext'; import BaseDialog from "../dialogs/BaseDialog"; @@ -34,6 +35,7 @@ import { getGeoUri } from '../../../utils/beacon'; import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; +import DialogSidebar from './DialogSidebar'; interface IProps extends IDialogProps { roomId: Room['roomId']; @@ -64,6 +66,8 @@ const BeaconViewDialog: React.FC = ({ }) => { const liveBeacons = useLiveBeacons(roomId, matrixClient); + const [isSidebarOpen, setSidebarOpen] = useState(false); + const bounds = getBeaconBounds(liveBeacons); const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds); @@ -108,6 +112,18 @@ const BeaconViewDialog: React.FC = ({
    } + { isSidebarOpen ? + setSidebarOpen(false)} /> : + setSidebarOpen(true)} + data-test-id='beacon-view-dialog-open-sidebar' + className='mx_BeaconViewDialog_viewListButton' + > +   + { _t('View list') } + + } ); diff --git a/src/components/views/beacon/DialogSidebar.tsx b/src/components/views/beacon/DialogSidebar.tsx new file mode 100644 index 00000000000..fac91c77cbc --- /dev/null +++ b/src/components/views/beacon/DialogSidebar.tsx @@ -0,0 +1,50 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { Beacon } from 'matrix-js-sdk/src/matrix'; + +import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg'; +import { _t } from '../../../languageHandler'; +import AccessibleButton from '../elements/AccessibleButton'; +import Heading from '../typography/Heading'; + +interface Props { + beacons: Beacon[]; + requestClose: () => void; +} + +const DialogSidebar: React.FC = ({ beacons, requestClose }) => { + return
    +
    + { _t('View List') } + + + +
    +
      + { /* TODO nice elements */ } + { beacons.map((beacon, index) =>
    1. { index }
    2. ) } +
    +
    ; +}; + +export default DialogSidebar; diff --git a/src/components/views/typography/Heading.tsx b/src/components/views/typography/Heading.tsx index 069ecb54df2..afc6c916c65 100644 --- a/src/components/views/typography/Heading.tsx +++ b/src/components/views/typography/Heading.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { HTMLAttributes } from 'react'; import classNames from 'classnames'; -type Size = 'h1' | 'h2' | 'h3'; +type Size = 'h1' | 'h2' | 'h3' | 'h4'; interface HeadingProps extends HTMLAttributes { size: Size; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 54fc8a8c8fc..24674408792 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2918,6 +2918,9 @@ "Live location ended": "Live location ended", "Live location error": "Live location error", "No live locations": "No live locations", + "View list": "View list", + "View List": "View List", + "Close sidebar": "Close sidebar", "An error occured whilst sharing your live location": "An error occured whilst sharing your live location", "You are sharing your live location": "You are sharing your live location", "%(timeRemaining)s left": "%(timeRemaining)s left", diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx index 197c0b7e7f5..7adfbf86c8b 100644 --- a/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -151,4 +151,43 @@ describe('', () => { expect(onFinished).toHaveBeenCalled(); }); + + describe('sidebar', () => { + it('opens sidebar on view list button click', () => { + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + + act(() => { + findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click'); + component.setProps({}); + }); + + expect(component.find('DialogSidebar').length).toBeTruthy(); + }); + + it('closes sidebar on close button click', () => { + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + + // open the sidebar + act(() => { + findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click'); + component.setProps({}); + }); + + expect(component.find('DialogSidebar').length).toBeTruthy(); + + // now close it + act(() => { + findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click'); + component.setProps({}); + }); + + expect(component.find('DialogSidebar').length).toBeFalsy(); + }); + }); }); diff --git a/test/components/views/beacon/DialogSidebar-test.tsx b/test/components/views/beacon/DialogSidebar-test.tsx new file mode 100644 index 00000000000..4c6fe8ad057 --- /dev/null +++ b/test/components/views/beacon/DialogSidebar-test.tsx @@ -0,0 +1,47 @@ +/* +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 { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import DialogSidebar from '../../../../src/components/views/beacon/DialogSidebar'; +import { findByTestId } from '../../../test-utils'; + +describe('', () => { + const defaultProps = { + beacons: [], + requestClose: jest.fn(), + }; + const getComponent = (props = {}) => + mount(); + + it('renders sidebar correctly', () => { + const component = getComponent(); + expect(component).toMatchSnapshot(); + }); + + it('closes on close button click', () => { + const requestClose = jest.fn(); + const component = getComponent({ requestClose }); + + act(() => { + findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click'); + }); + + expect(requestClose).toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap new file mode 100644 index 00000000000..e3b6f104907 --- /dev/null +++ b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders sidebar correctly 1`] = ` + +
    +
    + +

    + View List +

    +
    + +
    +
    +
    + +
    +
      +
    + +`; diff --git a/test/components/views/typography/Heading-test.tsx b/test/components/views/typography/Heading-test.tsx index 7f8561bfae3..186d15ff90f 100644 --- a/test/components/views/typography/Heading-test.tsx +++ b/test/components/views/typography/Heading-test.tsx @@ -25,4 +25,8 @@ describe('', () => { it('renders h3 with correct attributes', () => { expect(getComponent({ size: 'h3' })).toMatchSnapshot(); }); + + it('renders h4 with correct attributes', () => { + expect(getComponent({ size: 'h4' })).toMatchSnapshot(); + }); }); diff --git a/test/components/views/typography/__snapshots__/Heading-test.tsx.snap b/test/components/views/typography/__snapshots__/Heading-test.tsx.snap index 592ee050e8e..d9511fd4d97 100644 --- a/test/components/views/typography/__snapshots__/Heading-test.tsx.snap +++ b/test/components/views/typography/__snapshots__/Heading-test.tsx.snap @@ -32,3 +32,14 @@ exports[` renders h3 with correct attributes 1`] = `
    `; + +exports[` renders h4 with correct attributes 1`] = ` +

    +
    + test +
    +

    +`; From 65c74bd1580d5a8088ec2d9c2add9384326d5d65 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 20 Apr 2022 09:24:20 +0100 Subject: [PATCH 025/102] Bring `View Source` back from behind developer mode (#8369) --- .../views/context_menus/MessageContextMenu.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 55b893b784d..917091ece83 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -397,16 +397,14 @@ export default class MessageContextMenu extends React.Component ); } - let viewSourceButton: JSX.Element; - if (SettingsStore.getValue("developerMode")) { - viewSourceButton = ( - - ); - } + // This is specifically not behind the developerMode flag to give people insight into the Matrix + const viewSourceButton = ( + + ); let unhidePreviewButton: JSX.Element; if (eventTileOps?.isWidgetHidden()) { From 859fdf7d51cb04c7926c370aee90b007bdf86d67 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 20 Apr 2022 09:44:06 +0100 Subject: [PATCH 026/102] Cache localStorage objects for SettingsStore (#8366) --- .../AbstractLocalStorageSettingsHandler.ts | 87 +++++++++++++++++++ .../handlers/DeviceSettingsHandler.ts | 34 +++----- .../handlers/RoomDeviceSettingsHandler.ts | 23 ++--- 3 files changed, 109 insertions(+), 35 deletions(-) create mode 100644 src/settings/handlers/AbstractLocalStorageSettingsHandler.ts diff --git a/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts b/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts new file mode 100644 index 00000000000..5d64009b6fe --- /dev/null +++ b/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts @@ -0,0 +1,87 @@ +/* +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. +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 SettingsHandler from "./SettingsHandler"; + +/** + * Abstract settings handler wrapping around localStorage making getValue calls cheaper + * by caching the values and listening for localStorage updates from other tabs. + */ +export default abstract class AbstractLocalStorageSettingsHandler extends SettingsHandler { + private itemCache = new Map(); + private objectCache = new Map(); + + protected constructor() { + super(); + + // Listen for storage changes from other tabs to bust the cache + window.addEventListener("storage", (e: StorageEvent) => { + if (e.key === null) { + this.itemCache.clear(); + this.objectCache.clear(); + } else { + this.itemCache.delete(e.key); + this.objectCache.delete(e.key); + } + }); + } + + protected getItem(key: string): any { + if (!this.itemCache.has(key)) { + const value = localStorage.getItem(key); + this.itemCache.set(key, value); + return value; + } + + return this.itemCache.get(key); + } + + protected getObject(key: string): T | null { + if (!this.objectCache.has(key)) { + try { + const value = JSON.parse(localStorage.getItem(key)); + this.objectCache.set(key, value); + return value; + } catch (err) { + console.error("Failed to parse localStorage object", err); + return null; + } + } + + return this.objectCache.get(key) as T; + } + + protected setItem(key: string, value: any): void { + this.itemCache.set(key, value); + localStorage.setItem(key, value); + } + + protected setObject(key: string, value: object): void { + this.objectCache.set(key, value); + localStorage.setItem(key, JSON.stringify(value)); + } + + // handles both items and objects + protected removeItem(key: string): void { + localStorage.removeItem(key); + this.itemCache.delete(key); + this.objectCache.delete(key); + } + + public isSupported(): boolean { + return localStorage !== undefined && localStorage !== null; + } +} diff --git a/src/settings/handlers/DeviceSettingsHandler.ts b/src/settings/handlers/DeviceSettingsHandler.ts index 7d2fbaf236a..25c75c67a19 100644 --- a/src/settings/handlers/DeviceSettingsHandler.ts +++ b/src/settings/handlers/DeviceSettingsHandler.ts @@ -1,7 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd. -Copyright 2019 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. @@ -16,17 +16,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsHandler from "./SettingsHandler"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import { SettingLevel } from "../SettingLevel"; import { CallbackFn, WatchManager } from "../WatchManager"; +import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; /** * Gets and sets settings at the "device" level for the current device. * This handler does not make use of the roomId parameter. This handler * will special-case features to support legacy settings. */ -export default class DeviceSettingsHandler extends SettingsHandler { +export default class DeviceSettingsHandler extends AbstractLocalStorageSettingsHandler { /** * Creates a new device settings handler * @param {string[]} featureNames The names of known features. @@ -43,15 +43,15 @@ export default class DeviceSettingsHandler extends SettingsHandler { // Special case notifications if (settingName === "notificationsEnabled") { - const value = localStorage.getItem("notifications_enabled"); + const value = this.getItem("notifications_enabled"); if (typeof(value) === "string") return value === "true"; return null; // wrong type or otherwise not set } else if (settingName === "notificationBodyEnabled") { - const value = localStorage.getItem("notifications_body_enabled"); + const value = this.getItem("notifications_body_enabled"); if (typeof(value) === "string") return value === "true"; return null; // wrong type or otherwise not set } else if (settingName === "audioNotificationsEnabled") { - const value = localStorage.getItem("audio_notifications_enabled"); + const value = this.getItem("audio_notifications_enabled"); if (typeof(value) === "string") return value === "true"; return null; // wrong type or otherwise not set } @@ -68,15 +68,15 @@ export default class DeviceSettingsHandler extends SettingsHandler { // Special case notifications if (settingName === "notificationsEnabled") { - localStorage.setItem("notifications_enabled", newValue); + this.setItem("notifications_enabled", newValue); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); } else if (settingName === "notificationBodyEnabled") { - localStorage.setItem("notifications_body_enabled", newValue); + this.setItem("notifications_body_enabled", newValue); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); } else if (settingName === "audioNotificationsEnabled") { - localStorage.setItem("audio_notifications_enabled", newValue); + this.setItem("audio_notifications_enabled", newValue); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); } @@ -87,7 +87,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { delete settings["useIRCLayout"]; settings["layout"] = newValue; - localStorage.setItem("mx_local_settings", JSON.stringify(settings)); + this.setObject("mx_local_settings", settings); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); @@ -95,7 +95,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { const settings = this.getSettings() || {}; settings[settingName] = newValue; - localStorage.setItem("mx_local_settings", JSON.stringify(settings)); + this.setObject("mx_local_settings", settings); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); @@ -105,10 +105,6 @@ export default class DeviceSettingsHandler extends SettingsHandler { return true; // It's their device, so they should be able to } - public isSupported(): boolean { - return localStorage !== undefined && localStorage !== null; - } - public watchSetting(settingName: string, roomId: string, cb: CallbackFn) { this.watchers.watchSetting(settingName, roomId, cb); } @@ -118,9 +114,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { } private getSettings(): any { // TODO: [TS] Type return - const value = localStorage.getItem("mx_local_settings"); - if (!value) return null; - return JSON.parse(value); + return this.getObject("mx_local_settings"); } // Note: features intentionally don't use the same key as settings to avoid conflicts @@ -132,7 +126,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { return false; } - const value = localStorage.getItem("mx_labs_feature_" + featureName); + const value = this.getItem("mx_labs_feature_" + featureName); if (value === "true") return true; if (value === "false") return false; // Try to read the next config level for the feature. @@ -140,7 +134,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { } private writeFeature(featureName: string, enabled: boolean | null) { - localStorage.setItem("mx_labs_feature_" + featureName, `${enabled}`); + this.setItem("mx_labs_feature_" + featureName, `${enabled}`); this.watchers.notifyUpdate(featureName, null, SettingLevel.DEVICE, enabled); } } diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.ts b/src/settings/handlers/RoomDeviceSettingsHandler.ts index 47fcecdfacd..c1d1b57e9b6 100644 --- a/src/settings/handlers/RoomDeviceSettingsHandler.ts +++ b/src/settings/handlers/RoomDeviceSettingsHandler.ts @@ -1,6 +1,6 @@ /* Copyright 2017 Travis Ralston -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 - 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,15 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsHandler from "./SettingsHandler"; import { SettingLevel } from "../SettingLevel"; import { WatchManager } from "../WatchManager"; +import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; /** * Gets and sets settings at the "room-device" level for the current device in a particular * room. */ -export default class RoomDeviceSettingsHandler extends SettingsHandler { +export default class RoomDeviceSettingsHandler extends AbstractLocalStorageSettingsHandler { constructor(public readonly watchers: WatchManager) { super(); } @@ -32,7 +32,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { // Special case blacklist setting to use legacy values if (settingName === "blacklistUnverifiedDevices") { const value = this.read("mx_local_settings"); - if (value && value['blacklistUnverifiedDevicesPerRoom']) { + if (value?.['blacklistUnverifiedDevicesPerRoom']) { return value['blacklistUnverifiedDevicesPerRoom'][roomId]; } } @@ -49,16 +49,15 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { if (!value) value = {}; if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {}; value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue; - localStorage.setItem("mx_local_settings", JSON.stringify(value)); + this.setObject("mx_local_settings", value); this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue); return Promise.resolve(); } if (newValue === null) { - localStorage.removeItem(this.getKey(settingName, roomId)); + this.removeItem(this.getKey(settingName, roomId)); } else { - newValue = JSON.stringify({ value: newValue }); - localStorage.setItem(this.getKey(settingName, roomId), newValue); + this.setObject(this.getKey(settingName, roomId), { value: newValue }); } this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue); @@ -69,14 +68,8 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { return true; // It's their device, so they should be able to } - public isSupported(): boolean { - return localStorage !== undefined && localStorage !== null; - } - private read(key: string): any { - const rawValue = localStorage.getItem(key); - if (!rawValue) return null; - return JSON.parse(rawValue); + return this.getItem(key); } private getKey(settingName: string, roomId: string): string { From 2f6b76755c658e722ba55ae850b3c26ab384ee4c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 20 Apr 2022 11:05:55 +0100 Subject: [PATCH 027/102] Fix RightPanelStore handling first room on app launch wrong (#8370) --- src/stores/right-panel/RightPanelStore.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 1d505b9b22b..bb4ddc4fcbf 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -34,6 +34,7 @@ import { import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { ActiveRoomChangedPayload } from "../../dispatcher/payloads/ActiveRoomChangedPayload"; +import { RoomViewStore } from "../RoomViewStore"; /** * A class for tracking the state of the right panel between layouts and @@ -55,6 +56,7 @@ export default class RightPanelStore extends ReadyWatchingStore { } protected async onReady(): Promise { + this.viewedRoomId = RoomViewStore.instance.getRoomId(); this.matrixClient.on(CryptoEvent.VerificationRequest, this.onVerificationRequestUpdate); this.loadCacheFromSettings(); this.emitAndUpdateSettings(); @@ -348,6 +350,7 @@ export default class RightPanelStore extends ReadyWatchingStore { }; private handleViewedRoomChange(oldRoomId: Optional, newRoomId: Optional) { + if (!this.mxClient) return; // not ready, onReady will handle the first room this.viewedRoomId = newRoomId; // load values from byRoomCache with the viewedRoomId. this.loadCacheFromSettings(); From 4a38cbd550edf42477c901e5147527fd8326562c Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 20 Apr 2022 13:57:50 +0200 Subject: [PATCH 028/102] Live location sharing: beacon list view tiles (#8363) * add basic sidebar container Signed-off-by: Kerry Archibald * optionally show icon in beaconstatus Signed-off-by: Kerry Archibald * add avatar and style list item Signed-off-by: Kerry Archibald * formatted last update time Signed-off-by: Kerry Archibald * test beacon list item Signed-off-by: Kerry Archibald * move makeRoomWithState events to test utils Signed-off-by: Kerry Archibald * move beacon test helpers into utils Signed-off-by: Kerry Archibald * newline Signed-off-by: Kerry Archibald * add copyable text to beacon list item Signed-off-by: Kerry Archibald * add copyable geo uri to list item Signed-off-by: Kerry Archibald * improve spacing Signed-off-by: Kerry Archibald * overflow scroll on list Signed-off-by: Kerry Archibald --- res/css/_components.scss | 1 + .../views/beacon/_BeaconListItem.scss | 61 ++++++ .../views/beacon/_BeaconStatus.scss | 4 + .../views/beacon/_DialogSidebar.scss | 5 +- .../views/beacon/BeaconListItem.tsx | 82 +++++++++ src/components/views/beacon/BeaconStatus.tsx | 8 +- src/components/views/beacon/DialogSidebar.tsx | 4 +- .../views/beacon/OwnBeaconStatus.tsx | 1 + .../views/elements/CopyableText.tsx | 2 +- src/components/views/messages/MBeaconBody.tsx | 1 + src/i18n/strings/en_EN.json | 1 + src/utils/humanize.ts | 2 +- .../views/beacon/BeaconListItem-test.tsx | 173 ++++++++++++++++++ .../views/beacon/BeaconStatus-test.tsx | 6 + .../views/beacon/DialogSidebar-test.tsx | 1 - .../BeaconListItem-test.tsx.snap | 3 + .../__snapshots__/BeaconStatus-test.tsx.snap | 9 +- .../OwnBeaconStatus-test.tsx.snap | 1 + 18 files changed, 355 insertions(+), 10 deletions(-) create mode 100644 res/css/components/views/beacon/_BeaconListItem.scss create mode 100644 src/components/views/beacon/BeaconListItem.tsx create mode 100644 test/components/views/beacon/BeaconListItem-test.tsx create mode 100644 test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap diff --git a/res/css/_components.scss b/res/css/_components.scss index aa92057e633..7032c35f39c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -4,6 +4,7 @@ @import "./_font-sizes.scss"; @import "./_font-weights.scss"; @import "./_spacing.scss"; +@import "./components/views/beacon/_BeaconListItem.scss"; @import "./components/views/beacon/_BeaconStatus.scss"; @import "./components/views/beacon/_BeaconViewDialog.scss"; @import "./components/views/beacon/_DialogSidebar.scss"; diff --git a/res/css/components/views/beacon/_BeaconListItem.scss b/res/css/components/views/beacon/_BeaconListItem.scss new file mode 100644 index 00000000000..60311a4466f --- /dev/null +++ b/res/css/components/views/beacon/_BeaconListItem.scss @@ -0,0 +1,61 @@ +/* +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. +*/ + +.mx_BeaconListItem { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: flex-start; + padding: $spacing-12 0; + + border-bottom: 1px solid $system; +} + +.mx_BeaconListItem_avatarIcon { + flex: 0 0; + height: 32px; + width: 32px; +} + +.mx_BeaconListItem_avatar { + flex: 0 0; + box-sizing: border-box; + + margin-right: $spacing-8; + border: 2px solid $location-live-color; +} + +.mx_BeaconListItem_info { + flex: 1 1 0; + display: flex; + flex-direction: column; + align-items: stretch; +} + +.mx_BeaconListItem_status { + // override beacon status padding + padding: 0 !important; + margin-bottom: $spacing-8; + + .mx_BeaconStatus_label { + font-weight: $font-semi-bold; + } +} + +.mx_BeaconListItem_lastUpdated { + color: $tertiary-content; + font-size: $font-10px; +} diff --git a/res/css/components/views/beacon/_BeaconStatus.scss b/res/css/components/views/beacon/_BeaconStatus.scss index 8ac873604d2..4dd3d325475 100644 --- a/res/css/components/views/beacon/_BeaconStatus.scss +++ b/res/css/components/views/beacon/_BeaconStatus.scss @@ -59,3 +59,7 @@ limitations under the License. .mx_BeaconStatus_expiryTime { color: $secondary-content; } + +.mx_BeaconStatus_label { + margin-bottom: 2px; +} diff --git a/res/css/components/views/beacon/_DialogSidebar.scss b/res/css/components/views/beacon/_DialogSidebar.scss index 02d0e82cc33..1989b57c301 100644 --- a/res/css/components/views/beacon/_DialogSidebar.scss +++ b/res/css/components/views/beacon/_DialogSidebar.scss @@ -21,6 +21,9 @@ limitations under the License. height: 100%; width: 265px; + display: flex; + flex-direction: column; + box-sizing: border-box; padding: $spacing-16; @@ -34,7 +37,7 @@ limitations under the License. align-items: center; justify-content: space-between; - flex: 0; + flex: 0 0; margin-bottom: $spacing-16; color: $primary-content; diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx new file mode 100644 index 00000000000..eda1580700e --- /dev/null +++ b/src/components/views/beacon/BeaconListItem.tsx @@ -0,0 +1,82 @@ +/* +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, { useContext } from 'react'; +import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix'; +import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; + +import MatrixClientContext from '../../../contexts/MatrixClientContext'; +import { useEventEmitterState } from '../../../hooks/useEventEmitter'; +import { humanizeTime } from '../../../utils/humanize'; +import { _t } from '../../../languageHandler'; +import MemberAvatar from '../avatars/MemberAvatar'; +import CopyableText from '../elements/CopyableText'; +import BeaconStatus from './BeaconStatus'; +import { BeaconDisplayStatus } from './displayStatus'; +import StyledLiveBeaconIcon from './StyledLiveBeaconIcon'; + +interface Props { + beacon: Beacon; +} + +const BeaconListItem: React.FC = ({ beacon }) => { + const latestLocationState = useEventEmitterState( + beacon, + BeaconEvent.LocationUpdate, + () => beacon.latestLocationState, + ); + const matrixClient = useContext(MatrixClientContext); + const room = matrixClient.getRoom(beacon.roomId); + + if (!latestLocationState || !beacon.isLive) { + return null; + } + + const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self; + const beaconMember = isSelfLocation ? + room.getMember(beacon.beaconInfoOwner) : + undefined; + + const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp); + + return
  • + { isSelfLocation ? + : + + } +
    + + latestLocationState?.uri} + /> + + { _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) } +
    +
  • ; +}; + +export default BeaconListItem; diff --git a/src/components/views/beacon/BeaconStatus.tsx b/src/components/views/beacon/BeaconStatus.tsx index c9d7bd3762d..935e22f4f0b 100644 --- a/src/components/views/beacon/BeaconStatus.tsx +++ b/src/components/views/beacon/BeaconStatus.tsx @@ -28,6 +28,7 @@ import { formatTime } from '../../../DateUtils'; interface Props { displayStatus: BeaconDisplayStatus; displayLiveTimeRemaining?: boolean; + withIcon?: boolean; beacon?: Beacon; label?: string; } @@ -45,6 +46,7 @@ const BeaconStatus: React.FC> = label, className, children, + withIcon, ...rest }) => { const isIdle = displayStatus === BeaconDisplayStatus.Loading || @@ -54,11 +56,11 @@ const BeaconStatus: React.FC> = {...rest} className={classNames('mx_BeaconStatus', `mx_BeaconStatus_${displayStatus}`, className)} > - + /> }
    { displayStatus === BeaconDisplayStatus.Loading && { _t('Loading live location...') } } @@ -68,7 +70,7 @@ const BeaconStatus: React.FC> = { displayStatus === BeaconDisplayStatus.Active && beacon && <> <> - { label } + { label } { displayLiveTimeRemaining ? : diff --git a/src/components/views/beacon/DialogSidebar.tsx b/src/components/views/beacon/DialogSidebar.tsx index fac91c77cbc..4365b5fa8b6 100644 --- a/src/components/views/beacon/DialogSidebar.tsx +++ b/src/components/views/beacon/DialogSidebar.tsx @@ -21,6 +21,7 @@ import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; import Heading from '../typography/Heading'; +import BeaconListItem from './BeaconListItem'; interface Props { beacons: Beacon[]; @@ -41,8 +42,7 @@ const DialogSidebar: React.FC = ({ beacons, requestClose }) => {
      - { /* TODO nice elements */ } - { beacons.map((beacon, index) =>
    1. { index }
    2. ) } + { beacons.map((beacon) => ) }
    ; }; diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx index 204e2968293..0a682b11641 100644 --- a/src/components/views/beacon/OwnBeaconStatus.tsx +++ b/src/components/views/beacon/OwnBeaconStatus.tsx @@ -54,6 +54,7 @@ const OwnBeaconStatus: React.FC> = ({ displayStatus={ownDisplayStatus} label={_t('Live location enabled')} displayLiveTimeRemaining + withIcon {...rest} > { ownDisplayStatus === BeaconDisplayStatus.Active && string; border?: boolean; } diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index 4beac791019..bd7e10f044d 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -152,6 +152,7 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => beacon={beacon} displayStatus={displayStatus} label={_t('View live location')} + withIcon /> }
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 24674408792..c6a66c79678 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2913,6 +2913,7 @@ "Click for more info": "Click for more info", "Beta": "Beta", "Join the beta": "Join the beta", + "Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s", "Live until %(expiryTime)s": "Live until %(expiryTime)s", "Loading live location...": "Loading live location...", "Live location ended": "Live location ended", diff --git a/src/utils/humanize.ts b/src/utils/humanize.ts index 978d17424b3..47e2d83e8a0 100644 --- a/src/utils/humanize.ts +++ b/src/utils/humanize.ts @@ -30,7 +30,7 @@ const HOURS_1_DAY = 26; * @returns {string} The humanized time. */ export function humanizeTime(timeMillis: number): string { - const now = (new Date()).getTime(); + const now = Date.now(); let msAgo = now - timeMillis; const minutes = Math.abs(Math.ceil(msAgo / 60000)); const hours = Math.ceil(minutes / 60); diff --git a/test/components/views/beacon/BeaconListItem-test.tsx b/test/components/views/beacon/BeaconListItem-test.tsx new file mode 100644 index 00000000000..e7e9fbb7265 --- /dev/null +++ b/test/components/views/beacon/BeaconListItem-test.tsx @@ -0,0 +1,173 @@ +/* +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 { mount } from 'enzyme'; +import { + Beacon, + RoomMember, + MatrixEvent, +} from 'matrix-js-sdk/src/matrix'; +import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; +import { act } from 'react-dom/test-utils'; + +import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem'; +import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makeRoomWithBeacons, +} from '../../../test-utils'; + +describe('', () => { + // 14.03.2022 16:15 + const now = 1647270879403; + // go back in time to create beacons and locations in the past + jest.spyOn(global.Date, 'now').mockReturnValue(now - 600000); + const roomId = '!room:server'; + const aliceId = '@alice:server'; + + const mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue(aliceId), + getRoom: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + }); + + const aliceBeaconEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true }, + '$alice-room1-1', + ); + const alicePinBeaconEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true, assetType: LocationAssetType.Pin, description: "Alice's car" }, + '$alice-room1-1', + ); + const pinBeaconWithoutDescription = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true, assetType: LocationAssetType.Pin }, + '$alice-room1-1', + ); + + const aliceLocation1 = makeBeaconEvent( + aliceId, { beaconInfoId: aliceBeaconEvent.getId(), geoUri: 'geo:51,41', timestamp: now - 1 }, + ); + const aliceLocation2 = makeBeaconEvent( + aliceId, { beaconInfoId: aliceBeaconEvent.getId(), geoUri: 'geo:52,42', timestamp: now - 500000 }, + ); + + const defaultProps = { + beacon: new Beacon(aliceBeaconEvent), + }; + + const getComponent = (props = {}) => + mount(, { + wrappingComponent: MatrixClientContext.Provider, + wrappingComponentProps: { value: mockClient }, + }); + + const setupRoomWithBeacons = (beaconInfoEvents: MatrixEvent[], locationEvents?: MatrixEvent[]): Beacon[] => { + const beacons = makeRoomWithBeacons(roomId, mockClient, beaconInfoEvents, locationEvents); + + const member = new RoomMember(roomId, aliceId); + member.name = `Alice`; + const room = mockClient.getRoom(roomId); + jest.spyOn(room, 'getMember').mockReturnValue(member); + + return beacons; + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(Date, 'now').mockReturnValue(now); + }); + + it('renders null when beacon is not live', () => { + const notLiveBeacon = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: false }, + ); + const [beacon] = setupRoomWithBeacons([notLiveBeacon]); + const component = getComponent({ beacon }); + expect(component.html()).toBeNull(); + }); + + it('renders null when beacon has no location', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent]); + const component = getComponent({ beacon }); + expect(component.html()).toBeNull(); + }); + + describe('when a beacon is live and has locations', () => { + it('renders beacon info', () => { + const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.html()).toMatchSnapshot(); + }); + + describe('non-self beacons', () => { + it('uses beacon description as beacon name', () => { + const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('BeaconStatus').props().label).toEqual("Alice's car"); + }); + + it('uses beacon owner mxid as beacon name for a beacon without description', () => { + const [beacon] = setupRoomWithBeacons([pinBeaconWithoutDescription], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('BeaconStatus').props().label).toEqual(aliceId); + }); + + it('renders location icon', () => { + const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('StyledLiveBeaconIcon').length).toBeTruthy(); + }); + }); + + describe('self locations', () => { + it('renders beacon owner avatar', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('MemberAvatar').length).toBeTruthy(); + }); + + it('uses beacon owner name as beacon name', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('BeaconStatus').props().label).toEqual('Alice'); + }); + }); + + describe('on location updates', () => { + it('updates last updated time on location updated', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation2]); + const component = getComponent({ beacon }); + + expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated 9 minutes ago'); + + // update to a newer location + act(() => { + beacon.addLocations([aliceLocation1]); + component.setProps({}); + }); + + expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago'); + }); + }); + }); +}); diff --git a/test/components/views/beacon/BeaconStatus-test.tsx b/test/components/views/beacon/BeaconStatus-test.tsx index db4153defac..68a6a34a30f 100644 --- a/test/components/views/beacon/BeaconStatus-test.tsx +++ b/test/components/views/beacon/BeaconStatus-test.tsx @@ -26,6 +26,7 @@ describe('', () => { const defaultProps = { displayStatus: BeaconDisplayStatus.Loading, label: 'test label', + withIcon: true, }; const getComponent = (props = {}) => mount(); @@ -40,6 +41,11 @@ describe('', () => { expect(component).toMatchSnapshot(); }); + it('renders without icon', () => { + const component = getComponent({ withIcon: false, displayStatus: BeaconDisplayStatus.Stopped }); + expect(component.find('StyledLiveBeaconIcon').length).toBeFalsy(); + }); + describe('active state', () => { it('renders without children', () => { // mock for stable snapshot diff --git a/test/components/views/beacon/DialogSidebar-test.tsx b/test/components/views/beacon/DialogSidebar-test.tsx index 4c6fe8ad057..a5a1f0e5e79 100644 --- a/test/components/views/beacon/DialogSidebar-test.tsx +++ b/test/components/views/beacon/DialogSidebar-test.tsx @@ -41,7 +41,6 @@ describe('', () => { act(() => { findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click'); }); - expect(requestClose).toHaveBeenCalled(); }); }); diff --git a/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap new file mode 100644 index 00000000000..1518a60dba9 --- /dev/null +++ b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when a beacon is live and has locations renders beacon info 1`] = `"
  • Alice's carLive until 16:04
    Updated a few seconds ago
  • "`; diff --git a/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap index 5e2b6673daa..b3366336a17 100644 --- a/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconStatus-test.tsx.snap @@ -43,6 +43,7 @@ exports[` active state renders without children 1`] = ` } displayStatus="Active" label="test label" + withIcon={true} >
    active state renders without children 1`] = `
    - test label + + test label + renders loading state 1`] = `
    renders stopped state 1`] = `
    renders without a beacon instance 1`] = ` displayLiveTimeRemaining={true} displayStatus="Loading" label="Live location enabled" + withIcon={true} >
    Date: Wed, 20 Apr 2022 15:49:12 +0100 Subject: [PATCH 029/102] fix rainbow breaks compound emojis (#8245) * fix rainbow breaks compound emojis * use lodash split method for text splitting and add a unit test to test the behaviour * Update _RichText.scss * Update colour-test.ts --- src/utils/colour.ts | 4 +++- test/utils/colour-test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/utils/colour-test.ts diff --git a/src/utils/colour.ts b/src/utils/colour.ts index 10c18dbfe76..96eabd4eb40 100644 --- a/src/utils/colour.ts +++ b/src/utils/colour.ts @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { split } from 'lodash'; + export function textToHtmlRainbow(str: string): string { const frequency = (2 * Math.PI) / str.length; - return Array.from(str) + return split(str, '') .map((c, i) => { if (c === " ") { return c; diff --git a/test/utils/colour-test.ts b/test/utils/colour-test.ts new file mode 100644 index 00000000000..720c34e07bb --- /dev/null +++ b/test/utils/colour-test.ts @@ -0,0 +1,24 @@ +/* +Copyright 2022 Emmanuel Ezeka + +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 { textToHtmlRainbow } from "../../src/utils/colour"; + +describe("textToHtmlRainbow", () => { + it('correctly transform text to html without splitting the emoji in two', () => { + expect(textToHtmlRainbow('🐻')).toBe('🐻'); + expect(textToHtmlRainbow('🐕‍🦺')).toBe('🐕‍🦺'); + }); +}); From 6e86a14cc95d7afb2dd9b4763f6ba1431d60cbb5 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 20 Apr 2022 11:03:33 -0400 Subject: [PATCH 030/102] Show a lobby screen in video rooms (#8287) * Show a lobby screen in video rooms * Add connecting state * Test VideoRoomView * Test VideoLobby * Get the local video stream with useAsyncMemo * Clean up code review nits * Explicitly state what !important is overriding * Use spacing variables * Wait for video channel messaging * Update join button copy * Show frame on both the lobby and widget * Force dark theme for video lobby * Wait for the widget to be ready * Make VideoChannelStore constructor private * Allow video lobby to shrink * Add invite button to video room header * Show connected members on lobby screen * Make avatars in video lobby clickable * Increase video channel store timeout * Fix Jitsi Meet getting wedged on startup in Chrome and Safari * Revert "Fix Jitsi Meet getting wedged on startup in Chrome and Safari" This reverts commit 9f77b8c227c1a5bffa5d91b0c48bf3bbc44d4cec. * Disable device buttons while connecting * Factor RoomFacePile into a separate file * Fix i18n lint * Fix switching video channels while connected * Properly limit number of connected members in face pile * Fix CSS lint --- res/css/_components.scss | 2 + res/css/structures/_RoomView.scss | 18 +- res/css/structures/_VideoRoomView.scss | 41 ++++ res/css/views/elements/_FacePile.scss | 3 +- res/css/views/rooms/_RoomHeader.scss | 4 + res/css/views/voip/_VideoLobby.scss | 174 +++++++++++++ res/themes/dark/css/_dark.scss | 4 + res/themes/legacy-dark/css/_legacy-dark.scss | 4 + .../legacy-light/css/_legacy-light.scss | 5 + res/themes/light/css/_light.scss | 5 + src/Lifecycle.ts | 3 - src/components/structures/RoomView.tsx | 27 +- src/components/structures/SpaceRoomView.tsx | 6 +- src/components/structures/VideoRoomView.tsx | 67 +++++ src/components/views/elements/FacePile.tsx | 105 +++----- .../views/elements/RoomFacePile.tsx | 107 ++++++++ src/components/views/rooms/RoomHeader.tsx | 11 + src/components/views/rooms/RoomTile.tsx | 57 ++++- src/components/views/voip/VideoLobby.tsx | 232 ++++++++++++++++++ src/i18n/strings/en_EN.json | 34 ++- src/stores/VideoChannelStore.ts | 216 ++++++++++------ src/stores/widgets/ElementWidgetActions.ts | 1 + src/stores/widgets/WidgetMessagingStore.ts | 37 ++- src/utils/VideoChannelUtils.ts | 13 +- .../structures/VideoRoomView-test.tsx | 78 ++++++ .../views/rooms/RoomHeader-test.tsx | 1 + test/components/views/rooms/RoomTile-test.tsx | 17 +- .../components/views/voip/VideoLobby-test.tsx | 167 +++++++++++++ test/stores/VideoChannelStore-test.ts | 130 +++++++--- test/test-utils/video.ts | 34 ++- 30 files changed, 1337 insertions(+), 266 deletions(-) create mode 100644 res/css/structures/_VideoRoomView.scss create mode 100644 res/css/views/voip/_VideoLobby.scss create mode 100644 src/components/structures/VideoRoomView.tsx create mode 100644 src/components/views/elements/RoomFacePile.tsx create mode 100644 src/components/views/voip/VideoLobby.tsx create mode 100644 test/components/structures/VideoRoomView-test.tsx create mode 100644 test/components/views/voip/VideoLobby-test.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 7032c35f39c..60cacf03836 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -52,6 +52,7 @@ @import "./structures/_ToastContainer.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_UserMenu.scss"; +@import "./structures/_VideoRoomView.scss"; @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; @@ -323,3 +324,4 @@ @import "./views/voip/_DialPadModal.scss"; @import "./views/voip/_PiPContainer.scss"; @import "./views/voip/_VideoFeed.scss"; +@import "./views/voip/_VideoLobby.scss"; diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 84e6041ecd5..c73068896db 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -211,21 +211,9 @@ hr.mx_RoomView_myReadMarker { opacity: 1; } -// Immersive widgets -.mx_RoomView_immersive { - .mx_RoomHeader_wrapper { - border: unset; - } - - .mx_AppTile { - margin: $container-gap-width; - margin-right: calc($container-gap-width / 2); - width: auto; - height: 100%; - padding-top: 33px; // to match the right panel chat heading - - border-radius: 8px; - } +// Rooms with immersive content +.mx_RoomView_immersive .mx_RoomHeader_wrapper { + border: unset; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { diff --git a/res/css/structures/_VideoRoomView.scss b/res/css/structures/_VideoRoomView.scss new file mode 100644 index 00000000000..d99b3f5894b --- /dev/null +++ b/res/css/structures/_VideoRoomView.scss @@ -0,0 +1,41 @@ +/* +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. +*/ + +.mx_VideoRoomView { + flex-grow: 1; + min-height: 0; + + display: flex; + flex-direction: column; + margin: $container-gap-width; + margin-right: calc($container-gap-width / 2); + + background-color: $header-panel-bg-color; + padding-top: 33px; // to match the right panel chat heading + border: 8px solid $header-panel-bg-color; + border-radius: 8px; + + .mx_AppTile { + width: auto; + height: 100%; + border: none; + } + + // While the lobby is shown, the widget needs to stay loaded but hidden in the background + .mx_VideoLobby ~ .mx_AppTile { + display: none; + } +} diff --git a/res/css/views/elements/_FacePile.scss b/res/css/views/elements/_FacePile.scss index 3e83446b0ee..90f1c590a14 100644 --- a/res/css/views/elements/_FacePile.scss +++ b/res/css/views/elements/_FacePile.scss @@ -20,7 +20,8 @@ limitations under the License. flex-direction: row-reverse; vertical-align: middle; - > .mx_FacePile_face + .mx_FacePile_face { + // Overlap the children + > * + * { margin-right: -8px; } diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 7d25ade6bac..85c139402be 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -217,6 +217,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/search-inset.svg'); } +.mx_RoomHeader_inviteButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); +} + .mx_RoomHeader_voiceCallButton::before { mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); diff --git a/res/css/views/voip/_VideoLobby.scss b/res/css/views/voip/_VideoLobby.scss new file mode 100644 index 00000000000..a708e79c90e --- /dev/null +++ b/res/css/views/voip/_VideoLobby.scss @@ -0,0 +1,174 @@ +/* +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. +*/ + +.mx_VideoLobby { + min-height: 0; + flex-grow: 1; + padding: $spacing-12; + color: $video-lobby-primary-content; + background-color: $video-lobby-background; + border-radius: 8px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: $spacing-32; + + .mx_FacePile { + width: fit-content; + margin: $spacing-8 auto 0; + + .mx_FacePile_faces .mx_BaseAvatar_image { + border-color: $video-lobby-background; + } + } + + .mx_VideoLobby_preview { + position: relative; + width: 100%; + max-width: 800px; + aspect-ratio: 1.5; + background-color: $video-lobby-system; + + border-radius: 20px; + overflow: hidden; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .mx_BaseAvatar { + margin: $spacing-20; + + // Override the explicit dimensions on the element so that this gets sized responsively + width: unset !important; + height: unset !important; + min-width: 0; + min-height: 0; + flex: 0 1 200px; + } + + video { + position: absolute; + top: 0; + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transform: scaleX(-1); // flip the image + background-color: black; + } + + .mx_VideoLobby_controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + + background-color: rgba($video-lobby-background, 0.9); + + display: flex; + justify-content: center; + gap: $spacing-24; + + .mx_VideoLobby_deviceButtonWrapper { + position: relative; + margin: 6px 0 10px; + + .mx_VideoLobby_deviceButton { + $size: 50px; + + width: $size; + height: $size; + + background-color: $video-lobby-primary-content; + border-radius: calc($size / 2); + + &::before { + content: ''; + display: inline-block; + mask-repeat: no-repeat; + mask-size: 20px; + mask-position: center; + background-color: $video-lobby-system; + height: 100%; + width: 100%; + } + + &.mx_VideoLobby_deviceButton_audio::before { + mask-image: url('$(res)/img/voip/call-view/mic-off.svg'); + } + + &.mx_VideoLobby_deviceButton_video::before { + mask-image: url('$(res)/img/voip/call-view/cam-off.svg'); + } + } + + .mx_VideoLobby_deviceListButton { + $size: 15px; + + position: absolute; + bottom: 0; + right: -2.5px; + width: $size; + height: $size; + + background-color: $video-lobby-primary-content; + border-radius: calc($size / 2); + + &::before { + content: ''; + display: inline-block; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + mask-size: $size; + mask-position: center; + background-color: $video-lobby-system; + height: 100%; + width: 100%; + } + } + + &.mx_VideoLobby_deviceButtonWrapper_active { + .mx_VideoLobby_deviceButton, + .mx_VideoLobby_deviceListButton { + background-color: $video-lobby-system; + + &::before { + background-color: $video-lobby-primary-content; + } + } + + .mx_VideoLobby_deviceButton { + &.mx_VideoLobby_deviceButton_audio::before { + mask-image: url('$(res)/img/voip/call-view/mic-on.svg'); + } + + &.mx_VideoLobby_deviceButton_video::before { + mask-image: url('$(res)/img/voip/call-view/cam-on.svg'); + } + } + } + } + } + } + + .mx_VideoLobby_joinButton { + padding-left: 50px; + padding-right: 50px; + } +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index aa95979a7db..38fd3a58da4 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -187,6 +187,10 @@ $call-view-button-off-foreground: $system; $call-view-button-off-background: $primary-content; $video-feed-secondary-background: $system; + +$video-lobby-system: $system; +$video-lobby-background: $background; +$video-lobby-primary-content: $primary-content; // ******************** // Location sharing diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 981495bd131..6f958c08fdb 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -120,6 +120,10 @@ $call-view-button-off-background: $primary-content; $video-feed-secondary-background: $system; +$video-lobby-system: $system; +$video-lobby-background: $background; +$video-lobby-primary-content: $primary-content; + $roomlist-filter-active-bg-color: $panel-actions; $roomlist-bg-color: $header-panel-bg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index d67b6243c7b..e1da4d277da 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -178,6 +178,11 @@ $call-view-button-off-background: $secondary-content; $video-feed-secondary-background: #394049; // XXX: Color from dark theme +// All of these are from dark theme +$video-lobby-system: #21262C; +$video-lobby-background: #15191E; +$video-lobby-primary-content: #FFFFFF; + $username-variant1-color: #368bd6; $username-variant2-color: #ac3ba8; $username-variant3-color: #03b381; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 1c933a61468..14ed62f7266 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -280,6 +280,11 @@ $call-view-button-off-background: $secondary-content; $video-feed-secondary-background: #394049; // XXX: Color from dark theme $voipcall-plinth-color: $system; + +// All of these are from dark theme +$video-lobby-system: #21262C; +$video-lobby-background: #15191E; +$video-lobby-primary-content: #FFFFFF; // ******************** // One-off colors diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index da4010c57ea..f91158c38aa 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -36,7 +36,6 @@ import dis from './dispatcher/dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import Modal from './Modal'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; -import VideoChannelStore from "./stores/VideoChannelStore"; import PlatformPeg from "./PlatformPeg"; import { sendLoginRequest } from "./Login"; import * as StorageManager from './utils/StorageManager'; @@ -807,7 +806,6 @@ async function startMatrixClient(startSyncing = true): Promise { IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.instance.start(); CallHandler.instance.start(); - if (SettingsStore.getValue("feature_video_rooms")) VideoChannelStore.instance.start(); // Start Mjolnir even though we haven't checked the feature flag yet. Starting // the thing just wastes CPU cycles, but should result in no actual functionality @@ -926,7 +924,6 @@ export function stopMatrixClient(unsetClient = true): void { UserActivity.sharedInstance().stop(); TypingStore.sharedInstance().reset(); Presence.stop(); - if (SettingsStore.getValue("feature_video_rooms")) VideoChannelStore.instance.stop(); ActiveWidgetStore.instance.stop(); IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index f53e75db0be..761dd9b496f 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -75,8 +75,7 @@ import EffectsOverlay from "../views/elements/EffectsOverlay"; import { containsEmoji } from '../../effects/utils'; import { CHAT_EFFECTS } from '../../effects'; import WidgetStore from "../../stores/WidgetStore"; -import { getVideoChannel } from "../../utils/VideoChannelUtils"; -import AppTile from "../views/elements/AppTile"; +import VideoRoomView from "./VideoRoomView"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import Notifier from "../../Notifier"; import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast"; @@ -1249,7 +1248,7 @@ export class RoomView extends React.Component { } }; - private onInviteButtonClick = () => { + private onInviteClick = () => { // open the room inviter dis.dispatch({ action: 'view_invite', @@ -1904,7 +1903,7 @@ export class RoomView extends React.Component { statusBar = ; @@ -2169,18 +2168,11 @@ export class RoomView extends React.Component { ; break; case MainSplitContentType.Video: { - const app = getVideoChannel(this.state.room.roomId); - if (!app) break; mainSplitContentClassName = "mx_MainSplit_video"; - mainSplitBody = ; + mainSplitBody = <> + + { previewBar } + ; } } const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName); @@ -2190,6 +2182,7 @@ export class RoomView extends React.Component { let onAppsClick = this.onAppsClick; let onForgetClick = this.onForgetClick; let onSearchClick = this.onSearchClick; + let onInviteClick = null; // Simplify the header for other main split types switch (this.state.mainSplitContentType) { @@ -2212,6 +2205,9 @@ export class RoomView extends React.Component { onAppsClick = null; onForgetClick = null; onSearchClick = null; + if (this.state.room.canInvite(this.context.credentials.userId)) { + onInviteClick = this.onInviteClick; + } } return ( @@ -2227,6 +2223,7 @@ export class RoomView extends React.Component { oobData={this.props.oobData} inRoom={myMembership === 'join'} onSearchClick={onSearchClick} + onInviteClick={onInviteClick} onForgetClick={(myMembership === "leave") ? onForgetClick : null} e2eStatus={this.state.e2eStatus} onAppsClick={this.state.hasPinnedWidgets ? onAppsClick : null} diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index aaf3e4e1358..1e9d5caa0cf 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -58,7 +58,7 @@ import { } from "../../utils/space"; import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; import MemberAvatar from "../views/avatars/MemberAvatar"; -import FacePile from "../views/elements/FacePile"; +import RoomFacePile from "../views/elements/RoomFacePile"; import { AddExistingToSpace, defaultDmsRenderer, @@ -298,7 +298,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
    } - { space.getJoinRule() === "public" && } + { space.getJoinRule() === "public" && }
    { joinButtons }
    @@ -454,7 +454,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
    - + { inviteButton } { settingsButton }
    diff --git a/src/components/structures/VideoRoomView.tsx b/src/components/structures/VideoRoomView.tsx new file mode 100644 index 00000000000..2695dafa798 --- /dev/null +++ b/src/components/structures/VideoRoomView.tsx @@ -0,0 +1,67 @@ +/* +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, { FC, useContext, useState, useMemo } from "react"; +import { logger } from "matrix-js-sdk/src/logger"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { useEventEmitter } from "../../hooks/useEventEmitter"; +import { getVideoChannel } from "../../utils/VideoChannelUtils"; +import WidgetStore from "../../stores/WidgetStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import VideoChannelStore, { VideoChannelEvent } from "../../stores/VideoChannelStore"; +import AppTile from "../views/elements/AppTile"; +import VideoLobby from "../views/voip/VideoLobby"; + +const VideoRoomView: FC<{ room: Room, resizing: boolean }> = ({ room, resizing }) => { + const cli = useContext(MatrixClientContext); + const store = VideoChannelStore.instance; + + // In case we mount before the WidgetStore knows about our Jitsi widget + const [widgetLoaded, setWidgetLoaded] = useState(false); + useEventEmitter(WidgetStore.instance, UPDATE_EVENT, (roomId: string) => { + if (roomId === null || roomId === room.roomId) setWidgetLoaded(true); + }); + + const app = useMemo(() => { + const app = getVideoChannel(room.roomId); + if (!app) logger.warn(`No video channel for room ${room.roomId}`); + return app; + }, [room, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps + + const [connected, setConnected] = useState(store.connected && store.roomId === room.roomId); + useEventEmitter(store, VideoChannelEvent.Connect, () => setConnected(store.roomId === room.roomId)); + useEventEmitter(store, VideoChannelEvent.Disconnect, () => setConnected(false)); + + if (!app) return null; + + return
    + { connected ? null : } + { /* We render the widget even if we're disconnected, so it stays loaded */ } + +
    ; +}; + +export default VideoRoomView; diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx index bb39cc79575..566eddbe07e 100644 --- a/src/components/views/elements/FacePile.tsx +++ b/src/components/views/elements/FacePile.tsx @@ -14,91 +14,46 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { FC, HTMLAttributes, ReactNode, useContext } from "react"; -import { Room } from "matrix-js-sdk/src/models/room"; +import React, { FC, HTMLAttributes, ReactNode } from "react"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import { sortBy } from "lodash"; import MemberAvatar from "../avatars/MemberAvatar"; -import { _t } from "../../../languageHandler"; -import DMRoomMap from "../../../utils/DMRoomMap"; -import TextWithTooltip from "../elements/TextWithTooltip"; -import { useRoomMembers } from "../../../hooks/useRoomMembers"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; - -const DEFAULT_NUM_FACES = 5; +import TooltipTarget from "./TooltipTarget"; +import TextWithTooltip from "./TextWithTooltip"; interface IProps extends HTMLAttributes { - room: Room; - onlyKnownUsers?: boolean; - numShown?: number; + members: RoomMember[]; + faceSize: number; + overflow: boolean; + tooltip?: ReactNode; + children?: ReactNode; } -const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length; - -const FacePile: FC = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }) => { - const cli = useContext(MatrixClientContext); - const isJoined = room.getMyMembership() === "join"; - let members = useRoomMembers(room); - const count = members.length; - - // sort users with an explicit avatar first - const iteratees = [member => member.getMxcAvatarUrl() ? 0 : 1]; - if (onlyKnownUsers) { - members = members.filter(isKnownMember); - } else { - // sort known users first - iteratees.unshift(member => isKnownMember(member) ? 0 : 1); - } - - // exclude ourselves from the shown members list - const shownMembers = sortBy(members.filter(m => m.userId !== cli.getUserId()), iteratees).slice(0, numShown); - if (shownMembers.length < 1) return null; - - // We reverse the order of the shown faces in CSS to simplify their visual overlap, - // reverse members in tooltip order to make the order between the two match up. - const commaSeparatedMembers = shownMembers.map(m => m.rawDisplayName).reverse().join(", "); +const FacePile: FC = ({ members, faceSize, overflow, tooltip, children, ...props }) => { + const faces = members.map( + tooltip ? + m => : + m => + + , + ); - let tooltip: ReactNode; - if (props.onClick) { - let subText: string; - if (isJoined) { - subText = _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers }); - } else { - subText = _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }); - } - - tooltip =
    -
    - { _t("View all %(count)s members", { count }) } -
    -
    - { subText } -
    -
    ; - } else { - if (isJoined) { - tooltip = _t("%(count)s members including you, %(commaSeparatedMembers)s", { - count: count - 1, - commaSeparatedMembers, - }); - } else { - tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", { - count, - commaSeparatedMembers, - }); - } - } + const pileContents = <> + { overflow ? : null } + { faces } + ; return
    - - { members.length > numShown ? : null } - { shownMembers.map(m => - ) } - - { onlyKnownUsers && - { _t("%(count)s people you know have already joined", { count: members.length }) } - } + { tooltip ? ( + + { pileContents } + + ) : ( +
    + { pileContents } +
    + ) } + { children }
    ; }; diff --git a/src/components/views/elements/RoomFacePile.tsx b/src/components/views/elements/RoomFacePile.tsx new file mode 100644 index 00000000000..0b88403fffd --- /dev/null +++ b/src/components/views/elements/RoomFacePile.tsx @@ -0,0 +1,107 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { FC, HTMLAttributes, ReactNode, useContext } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { sortBy } from "lodash"; + +import { _t } from "../../../languageHandler"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import FacePile from "./FacePile"; +import { useRoomMembers } from "../../../hooks/useRoomMembers"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; + +const DEFAULT_NUM_FACES = 5; + +const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length; + +interface IProps extends HTMLAttributes { + room: Room; + onlyKnownUsers?: boolean; + numShown?: number; +} + +const RoomFacePile: FC = ( + { room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }, +) => { + const cli = useContext(MatrixClientContext); + const isJoined = room.getMyMembership() === "join"; + let members = useRoomMembers(room); + const count = members.length; + + // sort users with an explicit avatar first + const iteratees = [member => member.getMxcAvatarUrl() ? 0 : 1]; + if (onlyKnownUsers) { + members = members.filter(isKnownMember); + } else { + // sort known users first + iteratees.unshift(member => isKnownMember(member) ? 0 : 1); + } + + // exclude ourselves from the shown members list + const shownMembers = sortBy(members.filter(m => m.userId !== cli.getUserId()), iteratees).slice(0, numShown); + if (shownMembers.length < 1) return null; + + // We reverse the order of the shown faces in CSS to simplify their visual overlap, + // reverse members in tooltip order to make the order between the two match up. + const commaSeparatedMembers = shownMembers.map(m => m.name).reverse().join(", "); + + let tooltip: ReactNode; + if (props.onClick) { + let subText: string; + if (isJoined) { + subText = _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers }); + } else { + subText = _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }); + } + + tooltip =
    +
    + { _t("View all %(count)s members", { count }) } +
    +
    + { subText } +
    +
    ; + } else { + if (isJoined) { + tooltip = _t("%(count)s members including you, %(commaSeparatedMembers)s", { + count: count - 1, + commaSeparatedMembers, + }); + } else { + tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", { + count, + commaSeparatedMembers, + }); + } + } + + return numShown} + tooltip={tooltip} + {...props} + > + { onlyKnownUsers && + { _t("%(count)s people you know have already joined", { count: members.length }) } + } + ; +}; + +export default RoomFacePile; diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 1cdd2e770e8..9983b6f39c3 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -53,6 +53,7 @@ interface IProps { oobData?: IOOBData; inRoom: boolean; onSearchClick: () => void; + onInviteClick: () => void; onForgetClick: () => void; onCallPlaced: (type: CallType) => void; onAppsClick: () => void; @@ -255,6 +256,16 @@ export default class RoomHeader extends React.Component { buttons.push(searchButton); } + if (this.props.onInviteClick && this.props.inRoom) { + const inviteButton = ; + buttons.push(inviteButton); + } + const rightRow =
    { buttons } diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 29476a55cf9..530b22571aa 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -61,6 +61,7 @@ import { RoomViewStore } from "../../../stores/RoomViewStore"; enum VideoStatus { Disconnected, + Connecting, Connected, } @@ -105,7 +106,16 @@ export default class RoomTile extends React.PureComponent { constructor(props: IProps) { super(props); - const videoConnected = VideoChannelStore.instance.roomId === this.props.room.roomId; + let videoStatus; + if (VideoChannelStore.instance.roomId === this.props.room.roomId) { + if (VideoChannelStore.instance.connected) { + videoStatus = VideoStatus.Connected; + } else { + videoStatus = VideoStatus.Connecting; + } + } else { + videoStatus = VideoStatus.Disconnected; + } this.state = { selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId, @@ -113,9 +123,9 @@ export default class RoomTile extends React.PureComponent { generalMenuPosition: null, // generatePreview() will return nothing if the user has previews disabled messagePreview: "", - videoStatus: videoConnected ? VideoStatus.Connected : VideoStatus.Disconnected, + videoStatus, videoMembers: getConnectedMembers(this.props.room.currentState), - jitsiParticipants: videoConnected ? VideoChannelStore.instance.participants : [], + jitsiParticipants: VideoChannelStore.instance.participants, }; this.generatePreview(); @@ -185,8 +195,9 @@ export default class RoomTile extends React.PureComponent { this.props.room.on(RoomEvent.Name, this.onRoomNameUpdate); this.props.room.currentState.on(RoomStateEvent.Events, this.updateVideoMembers); - VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoStatus); - VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoStatus); + VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.onConnectVideo); + VideoChannelStore.instance.on(VideoChannelEvent.StartConnect, this.onStartConnectVideo); + VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.onDisconnectVideo); if (VideoChannelStore.instance.roomId === this.props.room.roomId) { VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants); } @@ -204,8 +215,9 @@ export default class RoomTile extends React.PureComponent { this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate); this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate); - VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoStatus); - VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoStatus); + VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.onConnectVideo); + VideoChannelStore.instance.off(VideoChannelEvent.StartConnect, this.onStartConnectVideo); + VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.onDisconnectVideo); } private onAction = (payload: ActionPayload) => { @@ -586,15 +598,37 @@ export default class RoomTile extends React.PureComponent { private updateVideoStatus = () => { if (VideoChannelStore.instance.roomId === this.props.room?.roomId) { + if (VideoChannelStore.instance.connected) { + this.onConnectVideo(this.props.room?.roomId); + } else { + this.onStartConnectVideo(this.props.room?.roomId); + } + } else { + this.onDisconnectVideo(this.props.room?.roomId); + } + }; + + private onConnectVideo = (roomId: string) => { + if (roomId === this.props.room?.roomId) { this.setState({ videoStatus: VideoStatus.Connected }); VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants); - } else { + } + }; + + private onStartConnectVideo = (roomId: string) => { + if (roomId === this.props.room?.roomId) { + this.setState({ videoStatus: VideoStatus.Connecting }); + } + }; + + private onDisconnectVideo = (roomId: string) => { + if (roomId === this.props.room?.roomId) { this.setState({ videoStatus: VideoStatus.Disconnected }); VideoChannelStore.instance.off(VideoChannelEvent.Participants, this.updateJitsiParticipants); } }; - private updateJitsiParticipants = (participants: IJitsiParticipant[]) => { + private updateJitsiParticipants = (roomId: string, participants: IJitsiParticipant[]) => { this.setState({ jitsiParticipants: participants }); }; @@ -636,6 +670,11 @@ export default class RoomTile extends React.PureComponent { videoActive = false; participantCount = this.state.videoMembers.length; break; + case VideoStatus.Connecting: + videoText = _t("Connecting..."); + videoActive = true; + participantCount = this.state.videoMembers.length; + break; case VideoStatus.Connected: videoText = _t("Connected"); videoActive = true; diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx new file mode 100644 index 00000000000..84bc470273e --- /dev/null +++ b/src/components/views/voip/VideoLobby.tsx @@ -0,0 +1,232 @@ +/* +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, { FC, useState, useMemo, useRef, useEffect } from "react"; +import classNames from "classnames"; +import { logger } from "matrix-js-sdk/src/logger"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t } from "../../../languageHandler"; +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; +import { useStateToggle } from "../../../hooks/useStateToggle"; +import { useConnectedMembers } from "../../../utils/VideoChannelUtils"; +import VideoChannelStore from "../../../stores/VideoChannelStore"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../context_menus/IconizedContextMenu"; +import { aboveLeftOf, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; +import { Alignment } from "../elements/Tooltip"; +import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import FacePile from "../elements/FacePile"; +import MemberAvatar from "../avatars/MemberAvatar"; + +interface IDeviceButtonProps { + kind: string; + devices: MediaDeviceInfo[]; + setDevice: (device: MediaDeviceInfo) => void; + deviceListLabel: string; + active: boolean; + disabled: boolean; + toggle: () => void; + activeTitle: string; + inactiveTitle: string; +} + +const DeviceButton: FC = ({ + kind, devices, setDevice, deviceListLabel, active, disabled, toggle, activeTitle, inactiveTitle, +}) => { + // Depending on permissions, the browser might not let us know device labels, + // in which case there's nothing helpful we can display + const labelledDevices = useMemo(() => devices.filter(d => d.label.length), [devices]); + + const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); + let contextMenu; + if (menuDisplayed) { + const selectDevice = (device: MediaDeviceInfo) => { + setDevice(device); + closeMenu(); + }; + + const buttonRect = buttonRef.current.getBoundingClientRect(); + contextMenu = + + { labelledDevices.map(d => + selectDevice(d)} + />, + ) } + + ; + } + + if (!devices.length) return null; + + return
    + + { labelledDevices.length > 1 ? ( + + ) : null } + { contextMenu } +
    ; +}; + +const MAX_FACES = 8; + +const VideoLobby: FC<{ room: Room }> = ({ room }) => { + const [connecting, setConnecting] = useState(false); + const me = useMemo(() => room.getMember(room.myUserId), [room]); + const connectedMembers = useConnectedMembers(room.currentState); + const videoRef = useRef(); + + const devices = useAsyncMemo(async () => { + try { + return await navigator.mediaDevices.enumerateDevices(); + } catch (e) { + logger.warn(`Failed to get media device list: ${e}`); + return []; + } + }, [], []); + const audioDevices = useMemo(() => devices.filter(d => d.kind === "audioinput"), [devices]); + const videoDevices = useMemo(() => devices.filter(d => d.kind === "videoinput"), [devices]); + + const [selectedAudioDevice, selectAudioDevice] = useState(null); + const [selectedVideoDevice, selectVideoDevice] = useState(null); + + const audioDevice = selectedAudioDevice ?? audioDevices[0]; + const videoDevice = selectedVideoDevice ?? videoDevices[0]; + + const [audioActive, toggleAudio] = useStateToggle(true); + const [videoActive, toggleVideo] = useStateToggle(true); + + const videoStream = useAsyncMemo(async () => { + if (videoDevice && videoActive) { + try { + return await navigator.mediaDevices.getUserMedia({ + video: { deviceId: videoDevice.deviceId }, + }); + } catch (e) { + logger.error(`Failed to get stream for device ${videoDevice.deviceId}: ${e}`); + } + } + return null; + }, [videoDevice, videoActive]); + + useEffect(() => { + if (videoStream) { + const videoElement = videoRef.current; + videoElement.srcObject = videoStream; + videoElement.play(); + + return () => { + videoStream?.getTracks().forEach(track => track.stop()); + videoElement.srcObject = null; + }; + } + }, [videoStream]); + + const connect = async () => { + setConnecting(true); + try { + await VideoChannelStore.instance.connect( + room.roomId, audioActive ? audioDevice : null, videoActive ? videoDevice : null, + ); + } catch (e) { + logger.error(e); + setConnecting(false); + } + }; + + let facePile; + if (connectedMembers.length) { + const shownMembers = connectedMembers.slice(0, MAX_FACES); + const overflow = connectedMembers.length > shownMembers.length; + + facePile =
    + { _t("%(count)s people connected", { count: connectedMembers.length }) } + +
    ; + } + + return
    + { facePile } +
    + +
    + + { _t("Connect now") } + +
    ; +}; + +export default VideoLobby; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c6a66c79678..6c8a1a0a4fb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1018,6 +1018,15 @@ "Your camera is turned off": "Your camera is turned off", "Your camera is still enabled": "Your camera is still enabled", "Dial": "Dial", + "%(count)s people connected|other": "%(count)s people connected", + "%(count)s people connected|one": "%(count)s person connected", + "Audio devices": "Audio devices", + "Mute microphone": "Mute microphone", + "Unmute microphone": "Unmute microphone", + "Video devices": "Video devices", + "Turn off camera": "Turn off camera", + "Turn on camera": "Turn on camera", + "Connect now": "Connect now", "Dialpad": "Dialpad", "Mute the microphone": "Mute the microphone", "Unmute the microphone": "Unmute the microphone", @@ -1763,6 +1772,7 @@ "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", "Search": "Search", + "Invite": "Invite", "Start new chat": "Start new chat", "Invite to space": "Invite to space", "You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space", @@ -1787,7 +1797,6 @@ "Explore all public rooms": "Explore all public rooms", "%(count)s results|other": "%(count)s results", "%(count)s results|one": "%(count)s result", - "Invite": "Invite", "Add space": "Add space", "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", @@ -1865,6 +1874,7 @@ "Copy room link": "Copy room link", "Leave": "Leave", "Video": "Video", + "Connecting...": "Connecting...", "Connected": "Connected", "%(count)s participants|other": "%(count)s participants", "%(count)s participants|one": "1 participant", @@ -2298,17 +2308,6 @@ "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)ssent a hidden message", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)ssent %(count)s hidden messages", "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)ssent a hidden message", - "Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s", - "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", - "View all %(count)s members|other": "View all %(count)s members", - "View all %(count)s members|one": "View 1 member", - "%(count)s members including you, %(commaSeparatedMembers)s|other": "%(count)s members including you, %(commaSeparatedMembers)s", - "%(count)s members including you, %(commaSeparatedMembers)s|zero": "You", - "%(count)s members including you, %(commaSeparatedMembers)s|one": "%(count)s members including you and %(commaSeparatedMembers)s", - "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s members including %(commaSeparatedMembers)s", - "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", - "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", - "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "collapse": "collapse", "expand": "expand", "Rotate Left": "Rotate Left", @@ -2349,6 +2348,17 @@ "This address is available to use": "This address is available to use", "This address is already in use": "This address is already in use", "This address had invalid server or is already in use": "This address had invalid server or is already in use", + "Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", + "View all %(count)s members|other": "View all %(count)s members", + "View all %(count)s members|one": "View 1 member", + "%(count)s members including you, %(commaSeparatedMembers)s|other": "%(count)s members including you, %(commaSeparatedMembers)s", + "%(count)s members including you, %(commaSeparatedMembers)s|zero": "You", + "%(count)s members including you, %(commaSeparatedMembers)s|one": "%(count)s members including you and %(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s members including %(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", + "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Server Options": "Server Options", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.", "Join millions for free on the largest public server": "Join millions for free on the largest public server", diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 6bd1b621e4d..58e18cab985 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -14,23 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventEmitter } from "events"; -import { logger } from "matrix-js-sdk/src/logger"; +import EventEmitter from "events"; import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api"; -import { MatrixClientPeg } from "../MatrixClientPeg"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { ActionPayload } from "../dispatcher/payloads"; import { ElementWidgetActions } from "./widgets/ElementWidgetActions"; -import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore"; -import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "./ActiveWidgetStore"; +import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "./widgets/WidgetMessagingStore"; import { - VIDEO_CHANNEL, VIDEO_CHANNEL_MEMBER, IVideoChannelMemberContent, getVideoChannel, } from "../utils/VideoChannelUtils"; +import { timeout } from "../utils/promise"; import WidgetUtils from "../utils/WidgetUtils"; +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; export enum VideoChannelEvent { + StartConnect = "start_connect", Connect = "connect", Disconnect = "disconnect", Participants = "participants", @@ -43,10 +44,25 @@ export interface IJitsiParticipant { participantId: string; } +const TIMEOUT_MS = 16000; + +// Wait until an event is emitted satisfying the given predicate +const waitForEvent = async (emitter: EventEmitter, event: string, pred: (...args) => boolean = () => true) => { + let listener; + const wait = new Promise(resolve => { + listener = (...args) => { if (pred(...args)) resolve(); }; + emitter.on(event, listener); + }); + + const timedOut = await timeout(wait, false, TIMEOUT_MS) === false; + emitter.off(event, listener); + if (timedOut) throw new Error("Timed out"); +}; + /* * Holds information about the currently active video channel. */ -export default class VideoChannelStore extends EventEmitter { +export default class VideoChannelStore extends AsyncStoreWithClient { private static _instance: VideoChannelStore; public static get instance(): VideoChannelStore { @@ -56,65 +72,121 @@ export default class VideoChannelStore extends EventEmitter { return VideoChannelStore._instance; } - private readonly cli = MatrixClientPeg.get(); - private activeChannel: ClientWidgetApi; - private _roomId: string; - private _participants: IJitsiParticipant[]; - - public get roomId(): string { - return this._roomId; + private constructor() { + super(defaultDispatcher); } - public get participants(): IJitsiParticipant[] { - return this._participants; + protected async onAction(payload: ActionPayload): Promise { + // nothing to do } - public start = () => { - ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetUpdate); - }; + private activeChannel: ClientWidgetApi; - public stop = () => { - ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Update, this.onActiveWidgetUpdate); - }; + private _roomId: string; + public get roomId(): string { return this._roomId; } + private set roomId(value: string) { this._roomId = value; } + + private _connected = false; + public get connected(): boolean { return this._connected; } + private set connected(value: boolean) { this._connected = value; } + + private _participants: IJitsiParticipant[] = []; + public get participants(): IJitsiParticipant[] { return this._participants; } + private set participants(value: IJitsiParticipant[]) { this._participants = value; } + + public connect = async (roomId: string, audioDevice: MediaDeviceInfo, videoDevice: MediaDeviceInfo) => { + if (this.activeChannel) await this.disconnect(); - private setConnected = async (roomId: string) => { const jitsi = getVideoChannel(roomId); if (!jitsi) throw new Error(`No video channel in room ${roomId}`); - const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(jitsi)); - if (!messaging) throw new Error(`Failed to bind video channel in room ${roomId}`); + const jitsiUid = WidgetUtils.getWidgetUid(jitsi); + const messagingStore = WidgetMessagingStore.instance; + + let messaging = messagingStore.getMessagingForUid(jitsiUid); + if (!messaging) { + // The widget might still be initializing, so wait for it + try { + await waitForEvent( + messagingStore, + WidgetMessagingStoreEvent.StoreMessaging, + (uid: string, widgetApi: ClientWidgetApi) => { + if (uid === jitsiUid) { + messaging = widgetApi; + return true; + } + return false; + }, + ); + } catch (e) { + throw new Error(`Failed to bind video channel in room ${roomId}: ${e}`); + } + } + + if (!messagingStore.isWidgetReady(jitsiUid)) { + // Wait for the widget to be ready to receive our join event + try { + await waitForEvent( + messagingStore, + WidgetMessagingStoreEvent.WidgetReady, + (uid: string) => uid === jitsiUid, + ); + } catch (e) { + throw new Error(`Video channel in room ${roomId} never became ready: ${e}`); + } + } this.activeChannel = messaging; - this._roomId = roomId; - this._participants = []; + this.roomId = roomId; + // Participant data will come down the event pipeline quickly, so prepare in advance + messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + + this.emit(VideoChannelEvent.StartConnect, roomId); + + // Actually perform the join + const waitForJoin = waitForEvent( + messaging, + `action:${ElementWidgetActions.JoinCall}`, + (ev: CustomEvent) => { + this.ack(ev); + return true; + }, + ); + messaging.transport.send(ElementWidgetActions.JoinCall, { + audioDevice: audioDevice?.label, + videoDevice: videoDevice?.label, + }); + try { + await waitForJoin; + } catch (e) { + // If it timed out, clean up our advance preparations + this.activeChannel = null; + this.roomId = null; + messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); - this.activeChannel.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); - this.activeChannel.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + this.emit(VideoChannelEvent.Disconnect, roomId); - this.emit(VideoChannelEvent.Connect); + throw new Error(`Failed to join call in room ${roomId}: ${e}`); + } + + this.connected = true; + messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + + this.emit(VideoChannelEvent.Connect, roomId); // Tell others that we're connected, by adding our device to room state - await this.updateDevices(devices => Array.from(new Set(devices).add(this.cli.getDeviceId()))); + this.updateDevices(roomId, devices => Array.from(new Set(devices).add(this.matrixClient.getDeviceId()))); }; - private setDisconnected = async () => { - this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); - this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); - - this.activeChannel = null; - this._participants = null; + public disconnect = async () => { + if (!this.activeChannel) throw new Error("Not connected to any video channel"); + const waitForDisconnect = waitForEvent(this, VideoChannelEvent.Disconnect); + this.activeChannel.transport.send(ElementWidgetActions.HangupCall, {}); try { - // Tell others that we're disconnected, by removing our device from room state - await this.updateDevices(devices => { - const devicesSet = new Set(devices); - devicesSet.delete(this.cli.getDeviceId()); - return Array.from(devicesSet); - }); - } finally { - // Save this for last, since updateDevices needs the room ID - this._roomId = null; - this.emit(VideoChannelEvent.Disconnect); + await waitForDisconnect; // onHangup cleans up for us + } catch (e) { + throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`); } }; @@ -124,41 +196,41 @@ export default class VideoChannelStore extends EventEmitter { this.activeChannel.transport.reply(ev.detail, {}); }; - private updateDevices = async (fn: (devices: string[]) => string[]) => { - if (!this.roomId) { - logger.error("Tried to update devices while disconnected"); - return; - } - - const room = this.cli.getRoom(this.roomId); - const devicesState = room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER, this.cli.getUserId()); + private updateDevices = async (roomId: string, fn: (devices: string[]) => string[]) => { + const room = this.matrixClient.getRoom(roomId); + const devicesState = room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER, this.matrixClient.getUserId()); const devices = devicesState?.getContent()?.devices ?? []; - await this.cli.sendStateEvent( - this.roomId, VIDEO_CHANNEL_MEMBER, { devices: fn(devices) }, this.cli.getUserId(), + await this.matrixClient.sendStateEvent( + roomId, VIDEO_CHANNEL_MEMBER, { devices: fn(devices) }, this.matrixClient.getUserId(), ); }; private onHangup = async (ev: CustomEvent) => { this.ack(ev); - await this.setDisconnected(); + + this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + + const roomId = this.roomId; + this.activeChannel = null; + this.roomId = null; + this.connected = false; + this.participants = []; + + this.emit(VideoChannelEvent.Disconnect, roomId); + + // Tell others that we're disconnected, by removing our device from room state + await this.updateDevices(roomId, devices => { + const devicesSet = new Set(devices); + devicesSet.delete(this.matrixClient.getDeviceId()); + return Array.from(devicesSet); + }); }; private onParticipants = (ev: CustomEvent) => { - this._participants = ev.detail.data.participants as IJitsiParticipant[]; - this.emit(VideoChannelEvent.Participants, ev.detail.data.participants); + this.participants = ev.detail.data.participants as IJitsiParticipant[]; + this.emit(VideoChannelEvent.Participants, this.roomId, ev.detail.data.participants); this.ack(ev); }; - - private onActiveWidgetUpdate = async () => { - if (this.activeChannel) { - // We got disconnected from the previous video channel, so clean up - await this.setDisconnected(); - } - - // If the new active widget is a video channel, that means we joined - if (ActiveWidgetStore.instance.getPersistentWidgetId() === VIDEO_CHANNEL) { - await this.setConnected(ActiveWidgetStore.instance.getPersistentRoomId()); - } - }; } diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts index e58581ce92a..117c4b47f3a 100644 --- a/src/stores/widgets/ElementWidgetActions.ts +++ b/src/stores/widgets/ElementWidgetActions.ts @@ -18,6 +18,7 @@ import { IWidgetApiRequest } from "matrix-widget-api"; export enum ElementWidgetActions { ClientReady = "im.vector.ready", + WidgetReady = "io.element.widget_ready", JoinCall = "io.element.join", HangupCall = "im.vector.hangup", CallParticipants = "io.element.participants", diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts index 1766db27594..d954af6d609 100644 --- a/src/stores/widgets/WidgetMessagingStore.ts +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -14,14 +14,20 @@ * limitations under the License. */ -import { ClientWidgetApi, Widget } from "matrix-widget-api"; +import { ClientWidgetApi, Widget, IWidgetApiRequest } from "matrix-widget-api"; +import { ElementWidgetActions } from "./ElementWidgetActions"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { ActionPayload } from "../../dispatcher/payloads"; import { EnhancedMap } from "../../utils/maps"; import WidgetUtils from "../../utils/WidgetUtils"; +export enum WidgetMessagingStoreEvent { + StoreMessaging = "store_messaging", + WidgetReady = "widget_ready", +} + /** * Temporary holding store for widget messaging instances. This is eventually * going to be merged with a more complete WidgetStore, but for now it's @@ -31,6 +37,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { private static internalInstance = new WidgetMessagingStore(); private widgetMap = new EnhancedMap(); // + private readyWidgets = new Set(); // widgets that have sent a WidgetReady event public constructor() { super(defaultDispatcher); @@ -51,11 +58,22 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { public storeMessaging(widget: Widget, roomId: string, widgetApi: ClientWidgetApi) { this.stopMessaging(widget, roomId); - this.widgetMap.set(WidgetUtils.calcWidgetUid(widget.id, roomId), widgetApi); + const uid = WidgetUtils.calcWidgetUid(widget.id, roomId); + this.widgetMap.set(uid, widgetApi); + + widgetApi.once(`action:${ElementWidgetActions.WidgetReady}`, (ev: CustomEvent) => { + this.readyWidgets.add(uid); + this.emit(WidgetMessagingStoreEvent.WidgetReady, uid); + widgetApi.transport.reply(ev.detail, {}); // ack + }); + + this.emit(WidgetMessagingStoreEvent.StoreMessaging, uid, widgetApi); } public stopMessaging(widget: Widget, roomId: string) { - this.widgetMap.remove(WidgetUtils.calcWidgetUid(widget.id, roomId))?.stop(); + const uid = WidgetUtils.calcWidgetUid(widget.id, roomId); + this.widgetMap.remove(uid)?.stop(); + this.readyWidgets.delete(uid); } public getMessaging(widget: Widget, roomId: string): ClientWidgetApi { @@ -64,7 +82,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { /** * Stops the widget messaging instance for a given widget UID. - * @param {string} widgetId The widget UID. + * @param {string} widgetUid The widget UID. */ public stopMessagingByUid(widgetUid: string) { this.widgetMap.remove(widgetUid)?.stop(); @@ -72,11 +90,18 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { /** * Gets the widget messaging class for a given widget UID. - * @param {string} widgetId The widget UID. + * @param {string} widgetUid The widget UID. * @returns {ClientWidgetApi} The widget API, or a falsey value if not found. - * @deprecated Widget IDs are not globally unique. */ public getMessagingForUid(widgetUid: string): ClientWidgetApi { return this.widgetMap.get(widgetUid); } + + /** + * @param {string} widgetUid The widget UID. + * @returns {boolean} Whether the widget has issued an ElementWidgetActions.WidgetReady event. + */ + public isWidgetReady(widgetUid: string): boolean { + return this.readyWidgets.has(widgetUid); + } } diff --git a/src/utils/VideoChannelUtils.ts b/src/utils/VideoChannelUtils.ts index 0387f81b8eb..11a1a9a35f2 100644 --- a/src/utils/VideoChannelUtils.ts +++ b/src/utils/VideoChannelUtils.ts @@ -14,10 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { useState } from "react"; +import { throttle } from "lodash"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { RoomState } from "matrix-js-sdk/src/models/room-state"; +import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { useTypedEventEmitter } from "../hooks/useEventEmitter"; import WidgetStore, { IApp } from "../stores/WidgetStore"; import { WidgetType } from "../widgets/WidgetType"; import WidgetUtils from "./WidgetUtils"; @@ -45,3 +48,11 @@ export const getConnectedMembers = (state: RoomState): RoomMember[] => .filter(e => e.getContent()?.devices?.length) .map(e => state.getMember(e.getStateKey())) .filter(member => member?.membership === "join"); + +export const useConnectedMembers = (state: RoomState, throttleMs = 100) => { + const [members, setMembers] = useState(getConnectedMembers(state)); + useTypedEventEmitter(state, RoomStateEvent.Update, throttle(() => { + setMembers(getConnectedMembers(state)); + }, throttleMs, { leading: true, trailing: true })); + return members; +}; diff --git a/test/components/structures/VideoRoomView-test.tsx b/test/components/structures/VideoRoomView-test.tsx new file mode 100644 index 00000000000..11d747103d9 --- /dev/null +++ b/test/components/structures/VideoRoomView-test.tsx @@ -0,0 +1,78 @@ +/* +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 { mount } from "enzyme"; +import { act } from "react-dom/test-utils"; +import { MatrixWidgetType } from "matrix-widget-api"; + +import { stubClient, stubVideoChannelStore, mkRoom, wrapInMatrixClientContext } from "../../test-utils"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import { VIDEO_CHANNEL } 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"; +import AppTile from "../../../src/components/views/elements/AppTile"; + +const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView); + +describe("VideoRoomView", () => { + stubClient(); + jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{ + id: VIDEO_CHANNEL, + eventId: "$1:example.org", + roomId: "!1:example.org", + type: MatrixWidgetType.JitsiMeet, + url: "https://example.org", + name: "Video channel", + creatorUserId: "@alice:example.org", + avatar_url: null, + }]); + Object.defineProperty(navigator, "mediaDevices", { + value: { enumerateDevices: () => [] }, + }); + + const cli = MatrixClientPeg.get(); + const room = mkRoom(cli, "!1:example.org"); + + let store; + beforeEach(() => { + store = stubVideoChannelStore(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("shows lobby and keeps widget loaded when disconnected", async () => { + const view = mount(); + // Wait for state to settle + await act(async () => Promise.resolve()); + + expect(view.find(VideoLobby).exists()).toEqual(true); + expect(view.find(AppTile).exists()).toEqual(true); + }); + + it("only shows widget when connected", async () => { + store.connect("!1:example.org"); + const view = mount(); + // Wait for state to settle + await act(async () => Promise.resolve()); + + expect(view.find(VideoLobby).exists()).toEqual(false); + expect(view.find(AppTile).exists()).toEqual(true); + }); +}); diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index c8030ad7c9f..1037b0377ce 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -176,6 +176,7 @@ function render(room: Room, roomContext?: Partial): ReactWrapper { room={room} inRoom={true} onSearchClick={() => {}} + onInviteClick={null} onForgetClick={() => {}} onCallPlaced={(_type) => { }} onAppsClick={() => {}} diff --git a/test/components/views/rooms/RoomTile-test.tsx b/test/components/views/rooms/RoomTile-test.tsx index 4ac7a369b6d..d209c32f0f9 100644 --- a/test/components/views/rooms/RoomTile-test.tsx +++ b/test/components/views/rooms/RoomTile-test.tsx @@ -18,34 +18,23 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { mocked } from "jest-mock"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { stubClient, mockStateEventImplementation, mkRoom, - mkEvent, + mkVideoChannelMember, stubVideoChannelStore, } from "../../../test-utils"; import RoomTile from "../../../../src/components/views/rooms/RoomTile"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { DefaultTagID } from "../../../../src/stores/room-list/models"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; -import { VIDEO_CHANNEL_MEMBER } from "../../../../src/utils/VideoChannelUtils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import PlatformPeg from "../../../../src/PlatformPeg"; import BasePlatform from "../../../../src/BasePlatform"; -const mkVideoChannelMember = (userId: string, devices: string[]): MatrixEvent => mkEvent({ - event: true, - type: VIDEO_CHANNEL_MEMBER, - room: "!1:example.org", - user: userId, - skey: userId, - content: { devices }, -}); - describe("RoomTile", () => { jest.spyOn(PlatformPeg, 'get') .mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform); @@ -85,6 +74,10 @@ describe("RoomTile", () => { ); expect(tile.find(".mx_RoomTile_videoIndicator").text()).toEqual("Video"); + act(() => { store.startConnect("!1:example.org"); }); + tile.update(); + expect(tile.find(".mx_RoomTile_videoIndicator").text()).toEqual("Connecting..."); + act(() => { store.connect("!1:example.org"); }); tile.update(); expect(tile.find(".mx_RoomTile_videoIndicator").text()).toEqual("Connected"); diff --git a/test/components/views/voip/VideoLobby-test.tsx b/test/components/views/voip/VideoLobby-test.tsx new file mode 100644 index 00000000000..4e7afb12c44 --- /dev/null +++ b/test/components/views/voip/VideoLobby-test.tsx @@ -0,0 +1,167 @@ +/* +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 { mount } from "enzyme"; +import { act } from "react-dom/test-utils"; +import { mocked } from "jest-mock"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; + +import { + stubClient, + stubVideoChannelStore, + mkRoom, + mkVideoChannelMember, + mockStateEventImplementation, +} from "../../../test-utils"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import FacePile from "../../../../src/components/views/elements/FacePile"; +import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar"; +import VideoLobby from "../../../../src/components/views/voip/VideoLobby"; + +describe("VideoLobby", () => { + stubClient(); + Object.defineProperty(navigator, "mediaDevices", { + value: { + enumerateDevices: jest.fn(), + getUserMedia: () => null, + }, + }); + jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {}); + + const cli = MatrixClientPeg.get(); + const room = mkRoom(cli, "!1:example.org"); + + let store; + beforeEach(() => { + store = stubVideoChannelStore(); + mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("connected members", () => { + it("hides when no one is connected", async () => { + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false); + }); + + it("is shown when someone is connected", async () => { + mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([ + // A user connected from 2 devices + mkVideoChannelMember("@alice:example.org", ["device 1", "device 2"]), + // A disconnected user + mkVideoChannelMember("@bob:example.org", []), + // A user that claims to have a connected device, but has left the room + mkVideoChannelMember("@chris:example.org", ["device 1"]), + ])); + + mocked(room.currentState).getMember.mockImplementation(userId => ({ + userId, + membership: userId === "@chris:example.org" ? "leave" : "join", + name: userId, + rawDisplayName: userId, + roomId: "!1:example.org", + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + }) as unknown as RoomMember); + + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + // Only Alice should display as connected + const memberText = lobby.find(".mx_VideoLobby_connectedMembers").children().at(0).text(); + expect(memberText).toEqual("1 person connected"); + expect(lobby.find(FacePile).find(MemberAvatar).props().member.userId).toEqual("@alice:example.org"); + }); + }); + + describe("device buttons", () => { + it("hides when no devices are available", async () => { + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + expect(lobby.find("DeviceButton").children().exists()).toEqual(false); + }); + + it("hides device list when only one device is available", async () => { + mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([{ + deviceId: "1", + groupId: "1", + label: "Webcam", + kind: "videoinput", + toJSON: () => {}, + }]); + + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + expect(lobby.find(".mx_VideoLobby_deviceListButton").exists()).toEqual(false); + }); + + it("shows device list when multiple devices are available", async () => { + mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([ + { + deviceId: "1", + groupId: "1", + label: "Front camera", + kind: "videoinput", + toJSON: () => {}, + }, + { + deviceId: "2", + groupId: "1", + label: "Back camera", + kind: "videoinput", + toJSON: () => {}, + }, + ]); + + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + expect(lobby.find(".mx_VideoLobby_deviceListButton").exists()).toEqual(true); + }); + }); + + describe("join button", () => { + it("works", async () => { + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + act(() => { + lobby.find("AccessibleButton.mx_VideoLobby_joinButton").simulate("click"); + }); + expect(store.connect).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/stores/VideoChannelStore-test.ts b/test/stores/VideoChannelStore-test.ts index 7b23ce0f4bc..fc8752ec76f 100644 --- a/test/stores/VideoChannelStore-test.ts +++ b/test/stores/VideoChannelStore-test.ts @@ -14,24 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ClientWidgetApi, MatrixWidgetType } from "matrix-widget-api"; +import { mocked } from "jest-mock"; +import { Widget, ClientWidgetApi, MatrixWidgetType, IWidgetApiRequest } from "matrix-widget-api"; -import { stubClient, mkRoom } from "../test-utils"; +import { stubClient, setupAsyncStoreWithClient } from "../test-utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; -import WidgetStore from "../../src/stores/WidgetStore"; -import ActiveWidgetStore from "../../src/stores/ActiveWidgetStore"; +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", () => { - stubClient(); - mkRoom(MatrixClientPeg.get(), "!1:example.org"); + const store = VideoChannelStore.instance; - const videoStore = VideoChannelStore.instance; - const widgetStore = ActiveWidgetStore.instance; - - jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{ + const widget = { id: VIDEO_CHANNEL } as unknown as Widget; + const app = { id: VIDEO_CHANNEL, eventId: "$1:example.org", roomId: "!1:example.org", @@ -40,43 +38,103 @@ describe("VideoChannelStore", () => { name: "Video channel", creatorUserId: "@alice:example.org", avatar_url: null, - }]); - jest.spyOn(WidgetMessagingStore.instance, "getMessagingForUid").mockReturnValue({ - on: () => {}, - off: () => {}, - once: () => {}, - transport: { - send: () => {}, - reply: () => {}, - }, - } as unknown as ClientWidgetApi); + } as IApp; + // Set up mocks to simulate the remote end of the widget API + let messageSent: Promise; + let messageSendMock: () => void; + let onMock: (action: string, listener: (ev: CustomEvent) => void) => void; + let onceMock: (action: string, listener: (ev: CustomEvent) => void) => void; + let messaging: ClientWidgetApi; beforeEach(() => { - videoStore.start(); - }); + stubClient(); + const cli = MatrixClientPeg.get(); + setupAsyncStoreWithClient(WidgetMessagingStore.instance, cli); + setupAsyncStoreWithClient(store, cli); + + let resolveMessageSent: () => void; + messageSent = new Promise(resolve => resolveMessageSent = resolve); + messageSendMock = jest.fn().mockImplementation(() => resolveMessageSent()); + onMock = jest.fn(); + onceMock = jest.fn(); - afterEach(() => { - videoStore.stop(); - jest.clearAllMocks(); + jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([app]); + messaging = { + on: onMock, + off: () => {}, + stop: () => {}, + once: onceMock, + transport: { + send: messageSendMock, + reply: () => {}, + }, + } as unknown as ClientWidgetApi; }); - it("tracks connection state", async () => { - expect(videoStore.roomId).toBeFalsy(); + const widgetReady = () => { + // Tell the WidgetStore that the widget is ready + const [, ready] = mocked(onceMock).mock.calls.find(([action]) => + action === `action:${ElementWidgetActions.WidgetReady}`, + ); + ready({ detail: {} } as unknown as CustomEvent); + }; + const confirmConnect = async () => { + // Wait for the store to contact the widget API + await messageSent; + // Then, locate the callback that will confirm the join + const [, join] = mocked(onMock).mock.calls.find(([action]) => + action === `action:${ElementWidgetActions.JoinCall}`, + ); + // Confirm the join, and wait for the store to update const waitForConnect = new Promise(resolve => - videoStore.once(VideoChannelEvent.Connect, resolve), + store.once(VideoChannelEvent.Connect, resolve), ); - widgetStore.setWidgetPersistence(VIDEO_CHANNEL, "!1:example.org", true); + join({ detail: {} } as unknown as CustomEvent); await waitForConnect; + }; - expect(videoStore.roomId).toEqual("!1:example.org"); - - const waitForDisconnect = new Promise(resolve => - videoStore.once(VideoChannelEvent.Disconnect, resolve), + const confirmDisconnect = async () => { + // Locate the callback that will perform the hangup + const [, hangup] = mocked(onceMock).mock.calls.find(([action]) => + action === `action:${ElementWidgetActions.HangupCall}`, ); - widgetStore.setWidgetPersistence(VIDEO_CHANNEL, "!1:example.org", false); - await waitForDisconnect; + // Hangup and wait for the store, once again + const waitForHangup = new Promise(resolve => + store.once(VideoChannelEvent.Disconnect, resolve), + ); + hangup({ detail: {} } as unknown as CustomEvent); + await waitForHangup; + }; + + it("connects and disconnects", async () => { + WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging); + widgetReady(); + expect(store.roomId).toBeFalsy(); + expect(store.connected).toEqual(false); + + store.connect("!1:example.org", null, null); + await confirmConnect(); + expect(store.roomId).toEqual("!1:example.org"); + expect(store.connected).toEqual(true); + + store.disconnect(); + await confirmDisconnect(); + expect(store.roomId).toBeFalsy(); + expect(store.connected).toEqual(false); + WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org"); + }); + + it("waits for messaging when connecting", async () => { + store.connect("!1:example.org", null, null); + WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging); + widgetReady(); + await confirmConnect(); + expect(store.roomId).toEqual("!1:example.org"); + expect(store.connected).toEqual(true); - expect(videoStore.roomId).toBeFalsy(); + store.disconnect(); + await confirmDisconnect(); + WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org"); }); }); diff --git a/test/test-utils/video.ts b/test/test-utils/video.ts index 91309452158..79c657a0c60 100644 --- a/test/test-utils/video.ts +++ b/test/test-utils/video.ts @@ -15,21 +15,34 @@ limitations under the License. */ import { EventEmitter } from "events"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import VideoChannelStore, { VideoChannelEvent } from "../../src/stores/VideoChannelStore"; +import { mkEvent } from "./test-utils"; +import { VIDEO_CHANNEL_MEMBER } from "../../src/utils/VideoChannelUtils"; +import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../src/stores/VideoChannelStore"; class StubVideoChannelStore extends EventEmitter { private _roomId: string; public get roomId(): string { return this._roomId; } + private _connected: boolean; + public get connected(): boolean { return this._connected; } + public get participants(): IJitsiParticipant[] { return []; } - public connect = (roomId: string) => { + public startConnect = (roomId: string) => { this._roomId = roomId; - this.emit(VideoChannelEvent.Connect); + this.emit(VideoChannelEvent.StartConnect, roomId); }; - public disconnect = () => { + public connect = jest.fn((roomId: string) => { + this._roomId = roomId; + this._connected = true; + this.emit(VideoChannelEvent.Connect, roomId); + }); + public disconnect = jest.fn(() => { + const roomId = this._roomId; this._roomId = null; - this.emit(VideoChannelEvent.Disconnect); - }; + this._connected = false; + this.emit(VideoChannelEvent.Disconnect, roomId); + }); } export const stubVideoChannelStore = (): StubVideoChannelStore => { @@ -37,3 +50,12 @@ export const stubVideoChannelStore = (): StubVideoChannelStore => { jest.spyOn(VideoChannelStore, "instance", "get").mockReturnValue(store as unknown as VideoChannelStore); return store; }; + +export const mkVideoChannelMember = (userId: string, devices: string[]): MatrixEvent => mkEvent({ + event: true, + type: VIDEO_CHANNEL_MEMBER, + room: "!1:example.org", + user: userId, + skey: userId, + content: { devices }, +}); From b4bcbb2f306bc5c564fda6d795da4f2fe7eb2335 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 20 Apr 2022 19:04:42 +0100 Subject: [PATCH 031/102] Fix race in Registration between server change and flows fetch (#8359) --- .../structures/auth/Registration.tsx | 39 +++++++++++-------- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index ecb4691e1b6..c5abe526f61 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -110,7 +110,9 @@ interface IState { } export default class Registration extends React.Component { - loginLogic: Login; + private readonly loginLogic: Login; + // `replaceClient` tracks latest serverConfig to spot when it changes under the async method which fetches flows + private latestServerConfig: ValidatedServerConfig; constructor(props) { super(props); @@ -149,26 +151,28 @@ export default class Registration extends React.Component { } private async replaceClient(serverConfig: ValidatedServerConfig) { + this.latestServerConfig = serverConfig; + const { hsUrl, isUrl } = serverConfig; + this.setState({ errorText: null, serverDeadError: null, serverErrorIsFatal: false, - // busy while we do liveness check (we need to avoid trying to render + // busy while we do live-ness check (we need to avoid trying to render // the UI auth component while we don't have a matrix client) busy: true, }); // Do a liveliness check on the URLs try { - await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( - serverConfig.hsUrl, - serverConfig.isUrl, - ); + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us this.setState({ serverIsAlive: true, serverErrorIsFatal: false, }); } catch (e) { + if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us this.setState({ busy: false, ...AutoDiscoveryUtils.authComponentStateForError(e, "register"), @@ -178,7 +182,6 @@ export default class Registration extends React.Component { } } - const { hsUrl, isUrl } = serverConfig; const cli = createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, @@ -190,8 +193,10 @@ export default class Registration extends React.Component { let ssoFlow: ISSOFlow; try { const loginFlows = await this.loginLogic.getFlows(); + if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us ssoFlow = loginFlows.find(f => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow; } catch (e) { + if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us logger.error("Failed to get login flows to check for SSO support", e); } @@ -200,23 +205,19 @@ export default class Registration extends React.Component { ssoFlow, busy: false, }); - const showGenericError = (e) => { - this.setState({ - errorText: _t("Unable to query for supported registration methods."), - // add empty flows array to get rid of spinner - flows: [], - }); - }; + try { // We do the first registration request ourselves to discover whether we need to // do SSO instead. If we've already started the UI Auth process though, we don't // need to. if (!this.state.doingUIAuth) { await this.makeRegisterRequest(null); + if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us // This should never succeed since we specified no auth object. logger.log("Expecting 401 from register request but got success!"); } } catch (e) { + if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us if (e.httpStatus === 401) { this.setState({ flows: e.data.flows, @@ -239,16 +240,20 @@ export default class Registration extends React.Component { } } else { logger.log("Unable to query for supported registration methods.", e); - showGenericError(e); + this.setState({ + errorText: _t("Unable to query for supported registration methods."), + // add empty flows array to get rid of spinner + flows: [], + }); } } } - private onFormSubmit = async (formVals): Promise => { + private onFormSubmit = async (formVals: Record): Promise => { this.setState({ errorText: "", busy: true, - formVals: formVals, + formVals, doingUIAuth: true, }); }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6c8a1a0a4fb..dcf6772ce80 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3209,8 +3209,8 @@ "Signing In...": "Signing In...", "If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while", "New? Create account": "New? Create account", - "Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", + "Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", "Someone already has that username, please try another.": "Someone already has that username, please try another.", "That e-mail address is already in use.": "That e-mail address is already in use.", From 605fbd3e4ef40e01123f384d1260a7de76f2c174 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 20 Apr 2022 19:24:34 +0000 Subject: [PATCH 032/102] Remove float property to let the margin between events appear on bubble message layout (#8373) * Add margin between events on bubble message layout Signed-off-by: Suguru Hirahara * Use more simplified way Because the float property prevents the margin from appearing, removing the property is more straightforward than adding other rules. Signed-off-by: Suguru Hirahara Co-authored-by: Kerry --- res/css/views/rooms/_EventBubbleTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index c2992589d0f..ca58c666ccf 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -154,7 +154,7 @@ limitations under the License. &[data-self=true] { .mx_EventTile_line { - float: right; + margin-inline-start: auto; border-bottom-left-radius: var(--cornerRadius); .mx_MImageBody .mx_MImageBody_thumbnail_container, From f2ec465f870eec7c7587363acd09b0dbeaa129ce Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 21 Apr 2022 09:06:57 +0200 Subject: [PATCH 033/102] Live location sharing: own live beacon status on maximised view (#8374) * add floating own live sharing eacon status to maximised view Signed-off-by: Kerry Archibald * add tests for own beacon status Signed-off-by: Kerry Archibald * stylelint Signed-off-by: Kerry Archibald * remove huge snapshot Signed-off-by: Kerry Archibald * remove unused emits from test Signed-off-by: Kerry Archibald --- res/css/_components.scss | 1 + .../views/beacon/_DialogOwnBeaconStatus.scss | 55 +++++++++++++ .../views/beacon/BeaconViewDialog.tsx | 2 + .../views/beacon/DialogOwnBeaconStatus.tsx | 80 +++++++++++++++++++ .../views/beacon/OwnBeaconStatus.tsx | 6 +- .../views/beacon/BeaconViewDialog-test.tsx | 34 +++++++- .../OwnBeaconStatus-test.tsx.snap | 13 +-- 7 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 res/css/components/views/beacon/_DialogOwnBeaconStatus.scss create mode 100644 src/components/views/beacon/DialogOwnBeaconStatus.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 60cacf03836..f4b833bdd47 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -7,6 +7,7 @@ @import "./components/views/beacon/_BeaconListItem.scss"; @import "./components/views/beacon/_BeaconStatus.scss"; @import "./components/views/beacon/_BeaconViewDialog.scss"; +@import "./components/views/beacon/_DialogOwnBeaconStatus.scss"; @import "./components/views/beacon/_DialogSidebar.scss"; @import "./components/views/beacon/_LeftPanelLiveShareWarning.scss"; @import "./components/views/beacon/_LiveTimeRemaining.scss"; diff --git a/res/css/components/views/beacon/_DialogOwnBeaconStatus.scss b/res/css/components/views/beacon/_DialogOwnBeaconStatus.scss new file mode 100644 index 00000000000..791e276f050 --- /dev/null +++ b/res/css/components/views/beacon/_DialogOwnBeaconStatus.scss @@ -0,0 +1,55 @@ +/* +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. +*/ + +.mx_DialogOwnBeaconStatus { + position: absolute; + bottom: $spacing-32; + width: 300px; + margin-left: -150px; + left: 50%; + + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: stretch; + + background: $background; + border-radius: 8px; + box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; + + padding: 0 $spacing-12; +} + +.mx_DialogOwnBeaconStatus_avatarIcon { + flex: 0 0; + height: 32px; + width: 32px; + margin: $spacing-8 0 $spacing-8 0; +} + +.mx_DialogOwnBeaconStatus_avatar { + flex: 0 0; + box-sizing: border-box; + + border: 2px solid $location-live-color; + margin: $spacing-8 0 $spacing-8 0; +} + +.mx_DialogOwnBeaconStatus_status { + flex: 1 1; + padding-right: 0; +} diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 76b9b75e3e4..9dc1352f105 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -36,6 +36,7 @@ import { Icon as LocationIcon } from '../../../../res/img/element-icons/location import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; import DialogSidebar from './DialogSidebar'; +import DialogOwnBeaconStatus from './DialogOwnBeaconStatus'; interface IProps extends IDialogProps { roomId: Room['roomId']; @@ -124,6 +125,7 @@ const BeaconViewDialog: React.FC = ({ { _t('View list') } } + ); diff --git a/src/components/views/beacon/DialogOwnBeaconStatus.tsx b/src/components/views/beacon/DialogOwnBeaconStatus.tsx new file mode 100644 index 00000000000..6ae1e8f5b8f --- /dev/null +++ b/src/components/views/beacon/DialogOwnBeaconStatus.tsx @@ -0,0 +1,80 @@ +/* +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, { useContext } from 'react'; +import { Room, Beacon } from 'matrix-js-sdk/src/matrix'; +import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; + +import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore'; +import { useEventEmitterState } from '../../../hooks/useEventEmitter'; +import { OwnProfileStore } from '../../../stores/OwnProfileStore'; +import OwnBeaconStatus from './OwnBeaconStatus'; +import { BeaconDisplayStatus } from './displayStatus'; +import MatrixClientContext from '../../../contexts/MatrixClientContext'; +import MemberAvatar from '../avatars/MemberAvatar'; +import StyledLiveBeaconIcon from './StyledLiveBeaconIcon'; + +interface Props { + roomId: Room['roomId']; +} + +const useOwnBeacon = (roomId: Room['roomId']): Beacon | undefined => { + const ownBeacon = useEventEmitterState( + OwnProfileStore.instance, + OwnBeaconStoreEvent.LivenessChange, + () => { + const [ownBeaconId] = OwnBeaconStore.instance.getLiveBeaconIds(roomId); + return OwnBeaconStore.instance.getBeaconById(ownBeaconId); + }, + ); + + return ownBeacon; +}; + +const DialogOwnBeaconStatus: React.FC = ({ roomId }) => { + const beacon = useOwnBeacon(roomId); + + const matrixClient = useContext(MatrixClientContext); + const room = matrixClient.getRoom(roomId); + + if (!beacon?.isLive) { + return null; + } + + const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self; + const beaconMember = isSelfLocation ? + room.getMember(beacon.beaconInfoOwner) : + undefined; + + return
    + { isSelfLocation ? + : + + } + +
    ; +}; + +export default DialogOwnBeaconStatus; diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx index 0a682b11641..0cd1cfb49a0 100644 --- a/src/components/views/beacon/OwnBeaconStatus.tsx +++ b/src/components/views/beacon/OwnBeaconStatus.tsx @@ -25,7 +25,9 @@ import AccessibleButton from '../elements/AccessibleButton'; interface Props { displayStatus: BeaconDisplayStatus; + className?: string; beacon?: Beacon; + withIcon?: boolean; } /** @@ -33,7 +35,7 @@ interface Props { * for errors and actions available for users own live beacons */ const OwnBeaconStatus: React.FC> = ({ - beacon, displayStatus, className, ...rest + beacon, displayStatus, ...rest }) => { const { hasWireError, @@ -49,12 +51,10 @@ const OwnBeaconStatus: React.FC> = ({ displayStatus; return { ownDisplayStatus === BeaconDisplayStatus.Active && ', () => { // 14.03.2022 16:15 @@ -50,9 +52,10 @@ describe('', () => { getClientWellKnown: jest.fn().mockReturnValue({ [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, }), - getUserId: jest.fn().mockReturnValue(aliceId), + getUserId: jest.fn().mockReturnValue(bobId), getRoom: jest.fn(), isGuest: jest.fn().mockReturnValue(false), + getVisibleRooms: jest.fn().mockReturnValue([]), }); // make fresh rooms every time @@ -83,6 +86,10 @@ describe('', () => { const getComponent = (props = {}) => mount(); + beforeEach(() => { + jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore(); + }); + it('renders a map with markers', () => { const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); @@ -95,6 +102,31 @@ describe('', () => { expect(component.find('SmartMarker').length).toEqual(1); }); + it('does not render any own beacon status when user is not live sharing', () => { + // default event belongs to alice, we are bob + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + expect(component.find('DialogOwnBeaconStatus').html()).toBeNull(); + }); + + it('renders own beacon status when user is live sharing', () => { + // default event belongs to alice + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + // mock own beacon store to show default event as alice's live beacon + jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockReturnValue([beacon.identifier]); + jest.spyOn(OwnBeaconStore.instance, 'getBeaconById').mockReturnValue(beacon); + const component = getComponent(); + expect(component.find('MemberAvatar').length).toBeTruthy(); + expect(component.find('OwnBeaconStatus').props()).toEqual({ + beacon, displayStatus: BeaconDisplayStatus.Active, + className: 'mx_DialogOwnBeaconStatus_status', + }); + }); + it('updates markers on changes to beacons', () => { const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); diff --git a/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap b/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap index d34eedeb56e..d2751ba2d9d 100644 --- a/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap @@ -5,24 +5,13 @@ exports[` renders without a beacon instance 1`] = ` displayStatus="Loading" >
    - -
    -
    From 74cab0323fecd0ba2d9d1fe5e74d1e6510e57e04 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 08:08:03 +0100 Subject: [PATCH 034/102] Upgrade dependencies (#8362) * Upgrade dependencies * delint --- src/HtmlUtils.tsx | 1 + src/components/structures/auth/Login.tsx | 2 + yarn.lock | 1263 +++++++++++----------- 3 files changed, 632 insertions(+), 634 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 5a2318db176..ac26eccc718 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -240,6 +240,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to } return { tagName, attribs }; }, + // eslint-disable-next-line @typescript-eslint/naming-convention '*': function(tagName: string, attribs: sanitizeHtml.Attributes) { // Delete any style previously assigned, style is an allowedTag for font, span & img, // because attributes are stripped after transforming. diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 0b990664148..297e233444e 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -135,7 +135,9 @@ export default class LoginComponent extends React.PureComponent 'm.login.password': this.renderPasswordStep, // CAS and SSO are the same thing, modulo the url we link to + // eslint-disable-next-line @typescript-eslint/naming-convention 'm.login.cas': () => this.renderSsoStep("cas"), + // eslint-disable-next-line @typescript-eslint/naming-convention 'm.login.sso': () => this.renderSsoStep("sso"), }; } diff --git a/yarn.lock b/yarn.lock index d5f058af71f..703b4a218db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,14 +10,14 @@ "@actions/http-client" "^1.0.11" "@actions/github@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.0.tgz#1754127976c50bd88b2e905f10d204d76d1472f8" - integrity sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ== + version "5.0.1" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.1.tgz#5fdbe371d9a592038668be95d12421361585fba1" + integrity sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg== dependencies: "@actions/http-client" "^1.0.11" - "@octokit/core" "^3.4.0" - "@octokit/plugin-paginate-rest" "^2.13.3" - "@octokit/plugin-rest-endpoint-methods" "^5.1.1" + "@octokit/core" "^3.6.0" + "@octokit/plugin-paginate-rest" "^2.17.0" + "@octokit/plugin-rest-endpoint-methods" "^5.13.0" "@actions/http-client@^1.0.11": version "1.0.11" @@ -26,18 +26,19 @@ dependencies: tunnel "0.0.6" -"@ampproject/remapping@^2.0.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.0.tgz#72becdf17ee44b2d1ac5651fb12f1952c336fe23" - integrity sha512-d5RysTlJ7hmw5Tw4UxgxcY3lkMe92n8sXCcuLPAyIAHK6j8DefDwtGnVVDgOnv+RnEosulDJ9NPKQL27bDId0g== +"@ampproject/remapping@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" + integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== dependencies: "@jridgewell/trace-mapping" "^0.3.0" "@babel/cli@^7.12.10": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.17.0.tgz#9b932d8f08a2e218fcdd9bba456044eb0a2e0b2c" - integrity sha512-es10YH/ejXbg551vtnmEzIPe3MQRNOS644o3pf8vUr1tIeNzVNlP8BBvs1Eh7roh5A+k2fEHUas+ZptOWHA1fQ== + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.17.6.tgz#169e5935f1795f0b62ded5a2accafeedfe5c5363" + integrity sha512-l4w608nsDNlxZhiJ5tE3DbNmr61fIKMZ6fTBo171VEFuFMIYuJ3mHRhTLEkKKyvx2Mizkkv/0a8OJOnZqkKYNA== dependencies: + "@jridgewell/trace-mapping" "^0.3.4" commander "^4.0.1" convert-source-map "^1.1.0" fs-readdir-recursive "^1.1.0" @@ -56,30 +57,30 @@ dependencies: "@babel/highlight" "^7.16.7" -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4", "@babel/compat-data@^7.16.8": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" - integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" + integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== "@babel/core@>=7.9.0", "@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.2.tgz#2c77fc430e95139d816d39b113b31bf40fb22337" - integrity sha512-R3VH5G42VSDolRHyUO4V2cfag8WHcZyxdq5Z/m8Xyb92lW/Erm/6kM+XtRFGf3Mulre3mveni2NHfEUws8wSvw== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe" + integrity sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw== dependencies: - "@ampproject/remapping" "^2.0.0" + "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.0" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helpers" "^7.17.2" - "@babel/parser" "^7.17.0" + "@babel/generator" "^7.17.9" + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helpers" "^7.17.9" + "@babel/parser" "^7.17.9" "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.0" + "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.1.2" + json5 "^2.2.1" semver "^6.3.0" "@babel/eslint-parser@^7.12.10": @@ -92,16 +93,16 @@ semver "^6.3.0" "@babel/eslint-plugin@^7.12.10": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.16.5.tgz#d1770685160922059f5d8f101055e799b7cff391" - integrity sha512-R1p6RMyU1Xl1U/NNr+D4+HjkQzN5dQOX0MpjW9WLWhHDjhzN9gso96MxxOFvPh0fKF/mMH8TGW2kuqQ2eK2s9A== + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.17.7.tgz#4ee1d5b29b79130f3bb5a933358376bcbee172b8" + integrity sha512-JATUoJJXSgwI0T8juxWYtK1JSgoLpIGUsCHIv+NMXcUDA2vIe6nvAHR9vnuJgs/P1hOFw7vPwibixzfqBBLIVw== dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.17.0", "@babel/generator@^7.7.2": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.0.tgz#7bd890ba706cd86d3e2f727322346ffdbf98f65e" - integrity sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw== +"@babel/generator@^7.17.9", "@babel/generator@^7.7.2": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.9.tgz#f4af9fd38fa8de143c29fce3f71852406fc1e2fc" + integrity sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ== dependencies: "@babel/types" "^7.17.0" jsesc "^2.5.1" @@ -122,25 +123,25 @@ "@babel/helper-explode-assignable-expression" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" - integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" + integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== dependencies: - "@babel/compat-data" "^7.16.4" + "@babel/compat-data" "^7.17.7" "@babel/helper-validator-option" "^7.16.7" browserslist "^4.17.5" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7": - version "7.17.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz#9699f14a88833a7e055ce57dcd3ffdcd25186b21" - integrity sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ== +"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz#71835d7fb9f38bd9f1378e40a4c0902fdc2ea49d" + integrity sha512-kUjip3gruz6AJKOq5i3nC6CoCEEF/oHH3cp6tOZhB+IyyyPyW0g1Gfsxn3mkk6S08pIA2y8GQh609v9G/5sHVQ== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-member-expression-to-functions" "^7.17.7" "@babel/helper-optimise-call-expression" "^7.16.7" "@babel/helper-replace-supers" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" @@ -181,21 +182,13 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-function-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" - integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== +"@babel/helper-function-name@^7.16.7", "@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== dependencies: - "@babel/helper-get-function-arity" "^7.16.7" "@babel/template" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/helper-get-function-arity@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" - integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== - dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/helper-hoist-variables@^7.16.7": version "7.16.7" @@ -204,12 +197,12 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-member-expression-to-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz#42b9ca4b2b200123c3b7e726b0ae5153924905b0" - integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q== +"@babel/helper-member-expression-to-functions@^7.16.7", "@babel/helper-member-expression-to-functions@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": version "7.16.7" @@ -218,19 +211,19 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-module-transforms@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41" - integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== +"@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" + integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== dependencies: "@babel/helper-environment-visitor" "^7.16.7" "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" "@babel/helper-split-export-declaration" "^7.16.7" "@babel/helper-validator-identifier" "^7.16.7" "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.7" - "@babel/types" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" "@babel/helper-optimise-call-expression@^7.16.7": version "7.16.7" @@ -264,12 +257,12 @@ "@babel/traverse" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-simple-access@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" - integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== +"@babel/helper-simple-access@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/helper-skip-transparent-expression-wrappers@^7.16.0": version "7.16.0" @@ -305,28 +298,28 @@ "@babel/traverse" "^7.16.8" "@babel/types" "^7.16.8" -"@babel/helpers@^7.17.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417" - integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ== +"@babel/helpers@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" + integrity sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q== dependencies: "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.0" + "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" "@babel/highlight@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" - integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" + integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== dependencies: "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c" - integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef" + integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" @@ -362,11 +355,11 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-proposal-class-static-block@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz#712357570b612106ef5426d13dc433ce0f200c2a" - integrity sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw== + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c" + integrity sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-create-class-features-plugin" "^7.17.6" "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" @@ -427,11 +420,11 @@ "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz#94593ef1ddf37021a25bdcb5754c4a8d534b01d8" - integrity sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA== + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz#d9eb649a54628a51701aef7e0ea3d17e2b9dd390" + integrity sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw== dependencies: - "@babel/compat-data" "^7.16.4" + "@babel/compat-data" "^7.17.0" "@babel/helper-compilation-targets" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" @@ -665,9 +658,9 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-destructuring@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz#ca9588ae2d63978a4c29d3f33282d8603f618e23" - integrity sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A== + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1" + integrity sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -734,22 +727,22 @@ babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-commonjs@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe" - integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.9.tgz#274be1a2087beec0254d4abd4d86e52442e1e5b6" + integrity sha512-2TBFd/r2I6VlYn0YRTz2JdazS+FoUuQ2rIFHoAxtyP/0G3D82SBLaRq9rnUkpqlLg03Byfl/+M32mpxjO6KaPw== dependencies: - "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-module-transforms" "^7.17.7" "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-systemjs@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz#887cefaef88e684d29558c2b13ee0563e287c2d7" - integrity sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw== + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz#81fd834024fae14ea78fbe34168b042f38703859" + integrity sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw== dependencies: "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-module-transforms" "^7.17.7" "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-validator-identifier" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" @@ -813,15 +806,15 @@ "@babel/plugin-transform-react-jsx" "^7.16.7" "@babel/plugin-transform-react-jsx@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.16.7.tgz#86a6a220552afd0e4e1f0388a68a372be7add0d4" - integrity sha512-8D16ye66fxiE8m890w0BpPpngG9o9OVBBy0gH2E+2AR7qMR2ZpTYJEqLxAsoroenMId0p/wMW+Blc0meDgu0Ag== + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1" + integrity sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" "@babel/helper-module-imports" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-jsx" "^7.16.7" - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/plugin-transform-react-pure-annotations@^7.16.7": version "7.16.7" @@ -832,11 +825,11 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-regenerator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" - integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz#0a33c3a61cf47f45ed3232903683a0afd2d3460c" + integrity sha512-Lc2TfbxR1HOyn/c6b4Y/b6NHoTb67n/IoWLxTu4kC7h4KQnWlhCq2S8Tx0t2SVvv5Uu87Hs+6JEJ5kt2tYGylQ== dependencies: - regenerator-transform "^0.14.2" + regenerator-transform "^0.15.0" "@babel/plugin-transform-reserved-words@^7.16.7": version "7.16.7" @@ -1030,9 +1023,9 @@ "@babel/plugin-transform-typescript" "^7.16.7" "@babel/register@^7.12.10": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.0.tgz#8051e0b7cb71385be4909324f072599723a1f084" - integrity sha512-UNZsMAZ7uKoGHo1HlEXfteEOYssf64n/PNLHGqOKq/bgYcu/4LrQWAHJwSCb3BRZK8Hi5gkJdRcwrGTO2wtRCg== + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.7.tgz#5eef3e0f4afc07e25e847720e7b987ae33f08d0b" + integrity sha512-fg56SwvXRifootQEDQAu1mKdjh5uthPzdO0N6t358FktfL4XjAVXuH58ULoiW8mesxiOgNIrxiImqEwv0+hRRA== dependencies: clone-deep "^4.0.1" find-cache-dir "^2.0.0" @@ -1041,17 +1034,17 @@ source-map-support "^0.5.16" "@babel/runtime-corejs3@^7.10.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.2.tgz#fdca2cd05fba63388babe85d349b6801b008fd13" - integrity sha512-NcKtr2epxfIrNM4VOmPKO46TvDMCBhgi2CrSHaEarrz+Plk2K5r9QemmOFTGpZaoKnWoGH5MO+CzeRsih/Fcgg== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.9.tgz#3d02d0161f0fbf3ada8e88159375af97690f4055" + integrity sha512-WxYHHUWF2uZ7Hp1K+D1xQgbgkGUfA+5UPOegEXGt2Y5SMog/rYCVaifLZDbw8UkNXozEqqrZTy6bglL7xTaCOw== dependencies: core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" - integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== dependencies: regenerator-runtime "^0.13.4" @@ -1064,18 +1057,18 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/traverse@^7.12.12", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.17", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0", "@babel/traverse@^7.7.2": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.0.tgz#3143e5066796408ccc880a33ecd3184f3e75cd30" - integrity sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg== +"@babel/traverse@^7.12.12", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.17", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.9.tgz#1f9b207435d9ae4a8ed6998b2b82300d83c37a0d" + integrity sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw== dependencies: "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.0" + "@babel/generator" "^7.17.9" "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" "@babel/helper-hoist-variables" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.0" + "@babel/parser" "^7.17.9" "@babel/types" "^7.17.0" debug "^4.1.0" globals "^11.1.0" @@ -1101,6 +1094,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@cypress/request@^2.88.10": version "2.88.10" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" @@ -1134,15 +1132,15 @@ lodash.once "^4.1.1" "@eslint/eslintrc@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.1.0.tgz#583d12dbec5d4f22f333f9669f7d0b7c7815b4d3" - integrity sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" + integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== dependencies: ajv "^6.12.4" debug "^4.3.2" espree "^9.3.1" globals "^13.9.0" - ignore "^4.0.6" + ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.0.4" @@ -1380,19 +1378,19 @@ chalk "^4.0.0" "@jridgewell/resolve-uri@^3.0.3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz#b876e3feefb9c8d3aa84014da28b5e52a0640d72" - integrity sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" + integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.10" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz#baf57b4e2a690d4f38560171f91783656b7f8186" - integrity sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg== + version "1.4.11" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" + integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== -"@jridgewell/trace-mapping@^0.3.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.2.tgz#e051581782a770c30ba219634f2019241c5d3cde" - integrity sha512-9KzzH4kMjA2XmBRHfqG2/Vtl7s92l6uNDd0wW7frDE+EUvQFGqNXhWp0UGJjSkt3v2AYjzOZn1QO9XaTNJIt1Q== +"@jridgewell/trace-mapping@^0.3.0", "@jridgewell/trace-mapping@^0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" + integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1484,14 +1482,14 @@ dependencies: "@octokit/types" "^6.0.3" -"@octokit/core@^3.4.0", "@octokit/core@^3.5.1": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" - integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== +"@octokit/core@^3.5.1", "@octokit/core@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" + integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== dependencies: "@octokit/auth-token" "^2.4.4" "@octokit/graphql" "^4.5.8" - "@octokit/request" "^5.6.0" + "@octokit/request" "^5.6.3" "@octokit/request-error" "^2.0.5" "@octokit/types" "^6.0.3" before-after-hook "^2.2.0" @@ -1520,7 +1518,7 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== -"@octokit/plugin-paginate-rest@^2.13.3", "@octokit/plugin-paginate-rest@^2.16.8": +"@octokit/plugin-paginate-rest@^2.16.8", "@octokit/plugin-paginate-rest@^2.17.0": version "2.17.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7" integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== @@ -1532,7 +1530,7 @@ resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== -"@octokit/plugin-rest-endpoint-methods@^5.1.1", "@octokit/plugin-rest-endpoint-methods@^5.12.0": +"@octokit/plugin-rest-endpoint-methods@^5.12.0", "@octokit/plugin-rest-endpoint-methods@^5.13.0": version "5.13.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba" integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA== @@ -1549,7 +1547,7 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.6.0": +"@octokit/request@^5.6.0", "@octokit/request@^5.6.3": version "5.6.3" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A== @@ -1578,15 +1576,15 @@ dependencies: "@octokit/openapi-types" "^11.2.0" -"@peculiar/asn1-schema@^2.0.44": - version "2.0.44" - resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.44.tgz#dcb1b8f84a4dd5f07f674028beade9c3de43cc06" - integrity sha512-uaCnjQ9A9WwQSMuDJcNOCYEPXTahgKbFMvI7eMOMd8lXgx0J1eU7F3BoMsK5PFxa3dVUxjSQbaOjfgGoeHGgoQ== +"@peculiar/asn1-schema@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.1.0.tgz#b1c185e1bd65c269d693c569534880cedc8aa5fa" + integrity sha512-D6g4C5YRKC/iPujMAOXuZ7YGdaoMx8GsvWzfVSyx2LYeL38ECOKNywlYAuwbqQvON64lgsYdAujWQPX8hhoBLw== dependencies: "@types/asn1js" "^2.0.2" - asn1js "^2.1.1" + asn1js "^2.3.1" pvtsutils "^1.2.1" - tslib "^2.3.0" + tslib "^2.3.1" "@peculiar/json-schema@^1.1.12": version "1.1.12" @@ -1596,77 +1594,77 @@ tslib "^2.0.0" "@peculiar/webcrypto@^1.1.4": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.2.3.tgz#79268ef0a8068bed2a40fc33bc68b4d3546fe2cc" - integrity sha512-q7wDfZy3k/tpnsYB23/MyyDkjn6IdHh8w+xwoVMS5cu6CjVoFzngXDZEOOuSE4zus2yO6ciQhhHxd4XkLpwVnQ== + version "1.3.3" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.3.3.tgz#ccca15d27d64a63df013e6575834563aa5df0dbc" + integrity sha512-+jkp16Hp18HkphJlMtqsQKjyDWJBh0AhDuoB+vVakuIRbkBdaFb7v26Ldm25altjiYhCyQnR5NChHxwSTvbXJw== dependencies: - "@peculiar/asn1-schema" "^2.0.44" + "@peculiar/asn1-schema" "^2.1.0" "@peculiar/json-schema" "^1.1.12" - pvtsutils "^1.2.1" + pvtsutils "^1.2.2" tslib "^2.3.1" - webcrypto-core "^1.4.0" + webcrypto-core "^1.7.2" "@sentry/browser@^6.11.0": - version "6.17.5" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.17.5.tgz#939c2c45f2b9ff7b449eaae270b6d4313b77a6c1" - integrity sha512-3xZ+6HgEnnQpXEk+3YJyfHlsJzMbOBEnjs3ImAiJvKJLdPnRjhixbQwsiV9Fm2SG43o2bFABpGSE337rvy4JuA== + version "6.19.6" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.6.tgz#75be467667fffa1f4745382fc7a695568609c634" + integrity sha512-V5QyY1cO1iuFCI78dOFbHV7vckbeQEPPq3a5dGSXlBQNYnd9Ec5xoxp5nRNpWQPOZ8/Ixt9IgRxdqVTkWib51g== dependencies: - "@sentry/core" "6.17.5" - "@sentry/types" "6.17.5" - "@sentry/utils" "6.17.5" + "@sentry/core" "6.19.6" + "@sentry/types" "6.19.6" + "@sentry/utils" "6.19.6" tslib "^1.9.3" -"@sentry/core@6.17.5": - version "6.17.5" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.17.5.tgz#8455e8b4ba36fc50c0d95b25eba06286389663ee" - integrity sha512-G1bYvZsWM5n0QYbnv89a24HCXex3rMWUnHWFysNqcWmw2YfiQ9RX5SakGbesewBj2Br2XpaDH0pED3QBUrC7yA== +"@sentry/core@6.19.6": + version "6.19.6" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.6.tgz#7d4649d0148b5d0be1358ab02e2f869bf7363e9a" + integrity sha512-biEotGRr44/vBCOegkTfC9rwqaqRKIpFljKGyYU6/NtzMRooktqOhjmjmItNCMRknArdeaQwA8lk2jcZDXX3Og== dependencies: - "@sentry/hub" "6.17.5" - "@sentry/minimal" "6.17.5" - "@sentry/types" "6.17.5" - "@sentry/utils" "6.17.5" + "@sentry/hub" "6.19.6" + "@sentry/minimal" "6.19.6" + "@sentry/types" "6.19.6" + "@sentry/utils" "6.19.6" tslib "^1.9.3" -"@sentry/hub@6.17.5": - version "6.17.5" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.17.5.tgz#9985ad61ac71003315ccaca2a2d42eed0e35bf6e" - integrity sha512-jBRrG0v3nHrymyj13Dv28aRS6xgQjWup45E0rljeksCxDL9frc734C0QGzGjE2MG7vZWtvd2CgP8uNbgYpwlTw== +"@sentry/hub@6.19.6": + version "6.19.6" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.19.6.tgz#ada83ceca0827c49534edfaba018221bc1eb75e1" + integrity sha512-PuEOBZxvx3bjxcXmWWZfWXG+orojQiWzv9LQXjIgroVMKM/GG4QtZbnWl1hOckUj7WtKNl4hEGO2g/6PyCV/vA== dependencies: - "@sentry/types" "6.17.5" - "@sentry/utils" "6.17.5" + "@sentry/types" "6.19.6" + "@sentry/utils" "6.19.6" tslib "^1.9.3" -"@sentry/minimal@6.17.5": - version "6.17.5" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.17.5.tgz#8116de3a4e8619242f4970ccbb7eea303ef88c52" - integrity sha512-WY/IQh2tb4XDkvr/2/8LB0mO8W3cgL6S2Uv+YfVRqogGJRdg5wD67aQ9zypNMq+D84cPwRuR/+51Npj6daox4w== +"@sentry/minimal@6.19.6": + version "6.19.6" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.19.6.tgz#b6cced3708e25d322039e68ebdf8fadfa445bf7d" + integrity sha512-T1NKcv+HTlmd8EbzUgnGPl4ySQGHWMCyZ8a8kXVMZOPDzphN3fVIzkYzWmSftCWp0rpabXPt9aRF2mfBKU+mAQ== dependencies: - "@sentry/hub" "6.17.5" - "@sentry/types" "6.17.5" + "@sentry/hub" "6.19.6" + "@sentry/types" "6.19.6" tslib "^1.9.3" "@sentry/tracing@^6.11.0": - version "6.17.5" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.17.5.tgz#3bc25279d370b9771d1f8abe3d7d7e7a0f445c9f" - integrity sha512-SVIDFL/QbNAlv+Rhgq14TqtTBp9WGipkjQXXaD/Pbqcj3/Oil7ZHHKYesW8Z+gtKQs73oQ9a0nCgraLFcQ68PQ== - dependencies: - "@sentry/hub" "6.17.5" - "@sentry/minimal" "6.17.5" - "@sentry/types" "6.17.5" - "@sentry/utils" "6.17.5" + version "6.19.6" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.19.6.tgz#faa156886afe441730f03cf9ac9c4982044b7135" + integrity sha512-STZdlEtTBqRmPw6Vjkzi/1kGkGPgiX0zdHaSOhSeA2HXHwx7Wnfu7veMKxtKWdO+0yW9QZGYOYqp0GVf4Swujg== + dependencies: + "@sentry/hub" "6.19.6" + "@sentry/minimal" "6.19.6" + "@sentry/types" "6.19.6" + "@sentry/utils" "6.19.6" tslib "^1.9.3" -"@sentry/types@6.17.5", "@sentry/types@^6.10.0": - version "6.17.5" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.17.5.tgz#3b6a591c8059d577b51ba2e71f4717aadbea923f" - integrity sha512-mn7qKuOvmZRTomJ7BiJEw6DM8femAVQcuHa8hdvK1F6ldMfFVLen5Z2LYGE7iY36GEa1Ba/AGGEKyF8D29y2/Q== +"@sentry/types@6.19.6", "@sentry/types@^6.10.0": + version "6.19.6" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.6.tgz#70513f9dca05d23d7ab9c2a6cb08d4db6763ca67" + integrity sha512-QH34LMJidEUPZK78l+Frt3AaVFJhEmIi05Zf8WHd9/iTt+OqvCHBgq49DDr1FWFqyYWm/QgW/3bIoikFpfsXyQ== -"@sentry/utils@6.17.5": - version "6.17.5" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.17.5.tgz#8034ac309d5f59ec919c748c6f35d6d831546523" - integrity sha512-MMCFCNWW73HRnqPVRGGSaMfSxtvvlNDgu1JFAOT2vnNkuf0mXvH301lyrh4pFJfntrtXOOk4bnGMhyWRlPADdA== +"@sentry/utils@6.19.6": + version "6.19.6" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.6.tgz#2ddc9ef036c3847084c43d0e5a55e4646bdf9021" + integrity sha512-fAMWcsguL0632eWrROp/vhPgI7sBj/JROWVPzpabwVkm9z3m1rQm6iLFn4qfkZL8Ozy6NVZPXOQ7EXmeU24byg== dependencies: - "@sentry/types" "6.17.5" + "@sentry/types" "6.19.6" tslib "^1.9.3" "@sinonjs/commons@^1.7.0": @@ -1716,9 +1714,9 @@ integrity sha512-t4YHCgtD+ERvH0FyxvNlYwJ2ezhqw7t+Ygh4urQ7dJER8i185JPv6oIM3ey5YQmGN6Zp9EMbpohkjZi9t3UxwA== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7": - version "7.1.18" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" - integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ== + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -1742,9 +1740,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" - integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + version "7.17.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.0.tgz#7a9b80f712fe2052bc20da153ff1e552404d8e4b" + integrity sha512-r8aveDbd+rzGP+ykSdF3oPuTVRWRfbBiHl0rVDM2yNEmSMXfkObQLV46b4RnCv3Lra51OlfnZhkkFaDl2MIRaA== dependencies: "@babel/types" "^7.3.0" @@ -1783,17 +1781,17 @@ integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A== "@types/enzyme@^3.10.9": - version "3.10.11" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.11.tgz#8924bd92cc63ac1843e215225dfa8f71555fe814" - integrity sha512-LEtC7zXsQlbGXWGcnnmOI7rTyP+i1QzQv4Va91RKXDEukLDaNyxu0rXlfMiGEhJwfgTPCTb0R+Pnlj//oM9e/w== + version "3.10.12" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.12.tgz#ac4494801b38188935580642f772ad18f72c132f" + integrity sha512-xryQlOEIe1TduDWAOphR0ihfebKFSWOXpIsk+70JskCfRfW+xALdnJ0r1ZOTo85F9Qsjk6vtlU7edTYHbls9tA== dependencies: "@types/cheerio" "*" "@types/react" "*" "@types/escape-html@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.1.tgz#b19b4646915f0ae2c306bf984dc0a59c5cfc97ba" - integrity sha512-4mI1FuUUZiuT95fSVqvZxp/ssQK9zsa86S43h9x3zPOSU9BBJ+BfDkXwuaU7BfsD+e7U0/cUUfJFk3iW2M4okA== + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.2.tgz#072b7b13784fb3cee9c2450c22f36405983f5e3c" + integrity sha512-gaBLT8pdcexFztLSPRtriHeXY/Kn4907uOCZ4Q3lncFBkheAWOuNt53ypsF8szgxbEJ513UeBzcf4utN0EzEwA== "@types/events@^3.0.0": version "3.0.0" @@ -1873,9 +1871,9 @@ pretty-format "^26.0.0" "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/json5@^0.0.29": version "0.0.29" @@ -1883,9 +1881,9 @@ integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= "@types/lodash@^4.14.168": - version "4.14.178" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" - integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== "@types/mdast@^3.0.0": version "3.0.10" @@ -1905,19 +1903,14 @@ integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA== "@types/node@*": - version "17.0.16" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.16.tgz#e3733f46797b9df9e853ca9f719c8a6f7b84cd26" - integrity sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA== - -"@types/node@^14.14.22": - version "14.18.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.10.tgz#774f43868964f3cfe4ced1f5417fe15818a4eaea" - integrity sha512-6iihJ/Pp5fsFJ/aEDGyvT4pHGmCpq7ToQ/yf4bl5SbVAvwpspYJ+v3jO7n8UyjhQVHTy+KNszOozDdv+O6sovQ== + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" + integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== -"@types/node@^14.14.31": - version "14.18.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24" - integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A== +"@types/node@^14.14.22", "@types/node@^14.14.31": + version "14.18.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.13.tgz#6ad4d9db59e6b3faf98dcfe4ca9d2aec84443277" + integrity sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -1940,14 +1933,14 @@ integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== "@types/prettier@^2.1.5": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.3.tgz#a3c65525b91fca7da00ab1a3ac2b5a2a4afbffbf" - integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w== + version "2.6.0" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.0.tgz#efcbd41937f9ae7434c714ab698604822d890759" + integrity sha512-G/AdOadiZhnJp0jXCaBQU449W2h716OW/EoXeYkCytxKL06X1WCXB4DZpp8TpZ8eyIJVS1cw4lrlkkSYU21cDw== "@types/prop-types@*": - version "15.7.4" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" - integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== "@types/qrcode@^1.3.5": version "1.4.2" @@ -1971,9 +1964,9 @@ "@types/react" "*" "@types/react-redux@^7.1.20": - version "7.1.22" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.22.tgz#0eab76a37ef477cc4b53665aeaf29cb60631b72a" - integrity sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ== + version "7.1.24" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" + integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -1981,11 +1974,11 @@ redux "^4.0.0" "@types/react-test-renderer@^17.0.1": - version "17.0.1" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" - integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.2.tgz#5f800a39b12ac8d2a2149e7e1885215bcf4edbbf" + integrity sha512-+F1KONQTBHDBBhbHuT2GNydeMpPuviduXIVJRB7Y4nma4NR5DrTJfMMZ+jbhEHbpwL+Uqhs1WXh4KHiyrtYTPg== dependencies: - "@types/react" "*" + "@types/react" "^17" "@types/react-transition-group@^4.4.0": version "4.4.4" @@ -1994,7 +1987,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@17.0.14": +"@types/react@*", "@types/react@17.0.14", "@types/react@^17": version "17.0.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f" integrity sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ== @@ -2041,9 +2034,9 @@ integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== "@types/yargs-parser@*": - version "20.2.1" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" - integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^15.0.0": version "15.0.14" @@ -2072,13 +2065,13 @@ integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w== "@typescript-eslint/eslint-plugin@^5.6.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.11.0.tgz#3b866371d8d75c70f9b81535e7f7d3aa26527c7a" - integrity sha512-HJh33bgzXe6jGRocOj4FmefD7hRY4itgjzOrSs3JPrTNXsX7j5+nQPciAUj/1nZtwo2kAc3C75jZO+T23gzSGw== + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz#022531a639640ff3faafaf251d1ce00a2ef000a1" + integrity sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q== dependencies: - "@typescript-eslint/scope-manager" "5.11.0" - "@typescript-eslint/type-utils" "5.11.0" - "@typescript-eslint/utils" "5.11.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/type-utils" "5.20.0" + "@typescript-eslint/utils" "5.20.0" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -2087,83 +2080,83 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.11.0.tgz#b4fcaf65513f9b34bdcbffdda055724a5efb7e04" - integrity sha512-x0DCjetHZYBRovJdr3U0zG9OOdNXUaFLJ82ehr1AlkArljJuwEsgnud+Q7umlGDFLFrs8tU8ybQDFocp/eX8mQ== + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.20.0.tgz#4991c4ee0344315c2afc2a62f156565f689c8d0b" + integrity sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w== dependencies: - "@typescript-eslint/scope-manager" "5.11.0" - "@typescript-eslint/types" "5.11.0" - "@typescript-eslint/typescript-estree" "5.11.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.11.0.tgz#f5aef83ff253f457ecbee5f46f762298f0101e4b" - integrity sha512-z+K4LlahDFVMww20t/0zcA7gq/NgOawaLuxgqGRVKS0PiZlCTIUtX0EJbC0BK1JtR4CelmkPK67zuCgpdlF4EA== +"@typescript-eslint/scope-manager@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz#79c7fb8598d2942e45b3c881ced95319818c7980" + integrity sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg== dependencies: - "@typescript-eslint/types" "5.11.0" - "@typescript-eslint/visitor-keys" "5.11.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" -"@typescript-eslint/type-utils@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.11.0.tgz#58be0ba73d1f6ef8983d79f7f0bc2209b253fefe" - integrity sha512-wDqdsYO6ofLaD4DsGZ0jGwxp4HrzD2YKulpEZXmgN3xo4BHJwf7kq49JTRpV0Gx6bxkSUmc9s0EIK1xPbFFpIA== +"@typescript-eslint/type-utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz#151c21cbe9a378a34685735036e5ddfc00223be3" + integrity sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw== dependencies: - "@typescript-eslint/utils" "5.11.0" + "@typescript-eslint/utils" "5.20.0" debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/types@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.11.0.tgz#ba345818a2540fdf2755c804dc2158517ab61188" - integrity sha512-cxgBFGSRCoBEhvSVLkKw39+kMzUKHlJGVwwMbPcTZX3qEhuXhrjwaZXWMxVfxDgyMm+b5Q5b29Llo2yow8Y7xQ== +"@typescript-eslint/types@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c" + integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg== -"@typescript-eslint/typescript-estree@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.11.0.tgz#53f9e09b88368191e52020af77c312a4777ffa43" - integrity sha512-yVH9hKIv3ZN3lw8m/Jy5I4oXO4ZBMqijcXCdA4mY8ull6TPTAoQnKKrcZ0HDXg7Bsl0Unwwx7jcXMuNZc0m4lg== +"@typescript-eslint/typescript-estree@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz#ab73686ab18c8781bbf249c9459a55dc9417d6b0" + integrity sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w== dependencies: - "@typescript-eslint/types" "5.11.0" - "@typescript-eslint/visitor-keys" "5.11.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.11.0.tgz#d91548ef180d74c95d417950336d9260fdbe1dc5" - integrity sha512-g2I480tFE1iYRDyMhxPAtLQ9HAn0jjBtipgTCZmd9I9s11OV8CTsG+YfFciuNDcHqm4csbAgC2aVZCHzLxMSUw== +"@typescript-eslint/utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.20.0.tgz#b8e959ed11eca1b2d5414e12417fd94cae3517a5" + integrity sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.11.0" - "@typescript-eslint/types" "5.11.0" - "@typescript-eslint/typescript-estree" "5.11.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.11.0.tgz#888542381f1a2ac745b06d110c83c0b261487ebb" - integrity sha512-E8w/vJReMGuloGxJDkpPlGwhxocxOpSVgSvjiLO5IxZPmxZF30weOeJYyPSEACwM+X4NziYS9q+WkN/2DHYQwA== +"@typescript-eslint/visitor-keys@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz#70236b5c6b67fbaf8b2f58bf3414b76c1e826c2a" + integrity sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg== dependencies: - "@typescript-eslint/types" "5.11.0" + "@typescript-eslint/types" "5.20.0" eslint-visitor-keys "^3.0.0" "@wojtekmaj/enzyme-adapter-react-17@^0.6.1": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.6.tgz#2ee3d4956caea4de05e372e5d9f39b31becffe6a" - integrity sha512-gSfhg8CiL0Vwc2UgUblGVZIy7M0KyXaZsd8+QwzV8TSVRLkGyzdLtYEcs9wRWyQTsdmOd+oRGqbVgUX7AVJxug== + version "0.6.7" + resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.7.tgz#7784bd32f518b186218cebb26c98c852676f30b0" + integrity sha512-B+byiwi/T1bx5hcj9wc0fUL5Hlb5giSXJzcnEfJVl2j6dGV2NJfcxDBYX0WWwIxlzNiFz8kAvlkFWI2y/nscZQ== dependencies: - "@wojtekmaj/enzyme-adapter-utils" "^0.1.2" + "@wojtekmaj/enzyme-adapter-utils" "^0.1.4" enzyme-shallow-equal "^1.0.0" has "^1.0.0" prop-types "^15.7.0" react-is "^17.0.0" react-test-renderer "^17.0.0" -"@wojtekmaj/enzyme-adapter-utils@^0.1.2": +"@wojtekmaj/enzyme-adapter-utils@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.4.tgz#bcd411ad6e368f17dce5425582c2907104cdb1ad" integrity sha512-ARGIQSIIv3oBia1m5Ihn1VU0FGmft6KPe39SBKTb8p7LSXO23YI4kNtc4M/cKoIY7P+IYdrZcgMObvedyjoSQA== @@ -2174,9 +2167,9 @@ prop-types "^15.7.0" abab@^2.0.3, abab@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== acorn-globals@^6.0.0: version "6.0.0" @@ -2237,9 +2230,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d" - integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2362,7 +2355,7 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-includes@^3.1.3, array-includes@^3.1.4: +array-includes@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== @@ -2395,22 +2388,24 @@ array.prototype.filter@^1.0.0: is-string "^1.0.7" array.prototype.flat@^1.2.3, array.prototype.flat@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" - integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + version "1.3.0" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" + integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.19.0" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" array.prototype.flatmap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" - integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz#a7e8ed4225f4788a70cd910abcf0791e76a5534f" + integrity sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.19.0" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" arrify@^1.0.1: version "1.0.1" @@ -2429,10 +2424,10 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" -asn1js@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.2.0.tgz#d890fcdda86b8a005693df14a986bfb2c2069c57" - integrity sha512-oagLNqpfNv7CvmyMoexMDNyVDSiq1rya0AEUgcLlNHdHgNl6U/hi8xY370n5y+ZIFEXOx0J4B1qF2NDjMRxklA== +asn1js@^2.3.1, asn1js@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.3.2.tgz#1864f859f6e5dfd7350c0543f411e18963f30592" + integrity sha512-IYzujqcOk7fHaePpTyvD3KPAA0AjT3qZlaQAw76zmPPAV/XTjhO+tbHjbFbIQZIhw+fk9wCSfb0Z6K+JHe8Q2g== dependencies: pvutils latest @@ -2721,9 +2716,9 @@ bluebird@^3.5.0, bluebird@^3.7.2: integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== blurhash@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244" - integrity sha512-MXIPz6zwYUKayju+Uidf83KhH0vodZfeRl6Ich8Gu+KGl0JgKiFq9LsfqV7cVU5fKD/AotmduZqvOfrGKOfTaA== + version "1.1.5" + resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.5.tgz#3034104cd5dce5a3e5caa871ae2f0f1f2d0ab566" + integrity sha512-a+LO3A2DfxTaTztsmkbLYmUzUeApi0LZuKalwbNmqAHR6HhJGMt1qSV/R3wc+w4DL28holjqO3Bg74aUGavGjg== boolbase@^1.0.0: version "1.0.0" @@ -2754,7 +2749,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2771,15 +2766,15 @@ browser-request@^0.3.3: resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17" integrity sha1-ns5bWsqJopkyJC4Yv5M975h2zBc= -browserslist@^4.12.0, browserslist@^4.17.5, browserslist@^4.19.1: - version "4.19.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" - integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== +browserslist@^4.12.0, browserslist@^4.17.5, browserslist@^4.20.2: + version "4.20.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" + integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== dependencies: - caniuse-lite "^1.0.30001286" - electron-to-chromium "^1.4.17" + caniuse-lite "^1.0.30001317" + electron-to-chromium "^1.4.84" escalade "^3.1.1" - node-releases "^2.0.1" + node-releases "^2.0.2" picocolors "^1.0.0" bs58@^4.0.1: @@ -2884,10 +2879,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001286: - version "1.0.30001309" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001309.tgz#e0ee78b9bec0704f67304b00ff3c5c0c768a9f62" - integrity sha512-Pl8vfigmBXXq+/yUz1jUwULeq9xhMJznzdc/xwl4WclDAuebcTHVefpz8lE/bMI+UN7TOkSSe7B7RnZd6+dzjA== +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001317: + version "1.0.30001332" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd" + integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== capture-exit@^2.0.0: version "2.0.0" @@ -2944,15 +2939,15 @@ check-more-types@^2.24.0: integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= cheerio-select@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823" - integrity sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" + integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== dependencies: - css-select "^4.1.3" - css-what "^5.0.1" + css-select "^4.3.0" + css-what "^6.0.1" domelementtype "^2.2.0" - domhandler "^4.2.0" - domutils "^2.7.0" + domhandler "^4.3.1" + domutils "^2.8.0" cheerio@^1.0.0-rc.3, cheerio@^1.0.0-rc.9: version "1.0.0-rc.10" @@ -3018,12 +3013,12 @@ clean-stack@^2.0.0: integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== cli-color@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.1.tgz#93e3491308691f1e46beb78b63d0fb2585e42ba6" - integrity sha512-eBbxZF6fqPUNnf7CLAFOersUnyYzv83tHFLSlts+OAHsNendaqv2tHCq+/MO+b3Y+9JeoUlIvobyxG/Z8GNeOg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.2.tgz#e295addbae470800def0254183c648531cdf4e3f" + integrity sha512-g4JYjrTW9MGtCziFNjkqp3IMpGhnJyeB0lOtRPjQkYhXzKYr6tYnXKyEVnMzITxhpbahsEW9KsxOYIDKwcsIBw== dependencies: d "^1.0.1" - es5-ext "^0.10.53" + es5-ext "^0.10.59" es6-iterator "^2.0.3" memoizee "^0.4.15" timers-ext "^0.1.7" @@ -3036,13 +3031,13 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-table3@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" - integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== + version "0.6.2" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a" + integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw== dependencies: string-width "^4.2.0" optionalDependencies: - colors "1.4.0" + "@colors/colors" "1.5.0" cli-truncate@^2.1.0: version "2.1.0" @@ -3133,11 +3128,6 @@ colorette@^2.0.16: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== -colors@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -3208,17 +3198,17 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-compat@^3.20.2, core-js-compat@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.0.tgz#bcc86aa5a589cee358e7a7fa0a4979d5a76c3885" - integrity sha512-OSXseNPSK2OPJa6GdtkMz/XxeXx8/CJvfhQWTqd6neuUraujcL4jVsjkLQz1OWnax8xVQJnRPe0V2jqNWORA+A== + version "3.22.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.0.tgz#7ce17ab57c378be2c717c7c8ed8f82a50a25b3e4" + integrity sha512-WwA7xbfRGrk8BGaaHlakauVXrlYmAIkk8PNGb1FDQS+Rbrewc3pgFfwJFRw6psmJVAll7Px9UHRYE16oRQnwAQ== dependencies: - browserslist "^4.19.1" + browserslist "^4.20.2" semver "7.0.0" core-js-pure@^3.20.2: - version "3.21.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.0.tgz#819adc8dfb808205ce25b51d50591becd615db7e" - integrity sha512-VaJUunCZLnxuDbo1rNOzwbet9E1K9joiXS5+DQMPtgxd24wfsZbJZMMfQLGYMlCUvSxLfsRUUhoOR2x28mFfeg== + version "3.22.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.0.tgz#0eaa54b6d1f4ebb4d19976bb4916dfad149a3747" + integrity sha512-ylOC9nVy0ak1N+fPIZj00umoZHgUVqmucklP5RT5N+vJof38klKn8Ze6KGyvchdClvEBr6LcQqJpI216LUMqYA== core-js@^1.0.0: version "1.2.7" @@ -3296,21 +3286,21 @@ css-box-model@^1.2.0: dependencies: tiny-invariant "^1.0.6" -css-select@^4.1.3: - version "4.2.1" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" - integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== +css-select@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== dependencies: boolbase "^1.0.0" - css-what "^5.1.0" - domhandler "^4.3.0" + css-what "^6.0.1" + domhandler "^4.3.1" domutils "^2.8.0" nth-check "^2.0.1" -css-what@^5.0.1, css-what@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== csscolorparser@~1.0.3: version "1.0.3" @@ -3345,9 +3335,9 @@ cssstyle@^2.3.0: cssom "~0.3.6" csstype@^3.0.2: - version "3.0.10" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" - integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== + version "3.0.11" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" + integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== cypress@^9.5.4: version "9.5.4" @@ -3432,14 +3422,14 @@ date-names@^0.1.11: integrity sha512-IxxoeD9tdx8pXVcmqaRlPvrXIsSrSrIZzfzlOkm9u+hyzKp5Wk/odt9O/gd7Ockzy8n/WHeEpTVJ2bF3mMV4LA== dayjs@^1.10.4: - version "1.11.0" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.0.tgz#009bf7ef2e2ea2d5db2e6583d2d39a4b5061e805" - integrity sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug== + version "1.11.1" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.1.tgz#90b33a3dda3417258d48ad2771b415def6545eb0" + integrity sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA== debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -3496,11 +3486,12 @@ deepmerge@^4.2.2: integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== define-properties@^1.1.3, define-properties@~1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: - object-keys "^1.0.12" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" define-property@^0.2.5: version "0.2.5" @@ -3612,9 +3603,9 @@ dom-serializer@0: entities "^2.0.0" dom-serializer@^1.0.1, dom-serializer@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== dependencies: domelementtype "^2.0.1" domhandler "^4.2.0" @@ -3626,9 +3617,9 @@ domelementtype@1, domelementtype@^1.3.1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== domexception@^2.0.1: version "2.0.1" @@ -3644,10 +3635,10 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" - integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: domelementtype "^2.2.0" @@ -3659,7 +3650,7 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -domutils@^2.5.2, domutils@^2.7.0, domutils@^2.8.0: +domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== @@ -3681,10 +3672,10 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.4.17: - version "1.4.66" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.66.tgz#d7453d363dcd7b06ed1757adcde34d724e27b367" - integrity sha512-f1RXFMsvwufWLwYUxTiP7HmjprKXrqEWHiQkjAYa9DJeVIlZk5v8gBGcaV+FhtXLly6C1OTVzQY+2UQrACiLlg== +electron-to-chromium@^1.4.84: + version "1.4.113" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.113.tgz#b3425c086e2f4fc31e9e53a724c6f239e3adb8b9" + integrity sha512-s30WKxp27F3bBH6fA07FYL2Xm/FYnYrKpMjHr3XVCTUb9anAyZn/BeZfPWgTZGAbJeT4NxNwISSbLcYZvggPMA== emittery@^0.8.1: version "0.8.1" @@ -3814,10 +3805,10 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== +es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2: + version "1.19.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.5.tgz#a2cb01eb87f724e815b278b0dd0d00f36ca9a7f1" + integrity sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" @@ -3825,15 +3816,15 @@ es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1: get-intrinsic "^1.1.1" get-symbol-description "^1.0.0" has "^1.0.3" - has-symbols "^1.0.2" + has-symbols "^1.0.3" internal-slot "^1.0.3" is-callable "^1.2.4" - is-negative-zero "^2.0.1" + is-negative-zero "^2.0.2" is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" + is-shared-array-buffer "^1.0.2" is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" + is-weakref "^1.0.2" + object-inspect "^1.12.0" object-keys "^1.1.1" object.assign "^4.1.2" string.prototype.trimend "^1.0.4" @@ -3859,6 +3850,13 @@ es-get-iterator@^1.1.2: is-string "^1.0.5" isarray "^2.0.5" +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -3868,16 +3866,16 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.59, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.60" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.60.tgz#e8060a86472842b93019c31c34865012449883f4" + integrity sha512-jpKNXIt60htYG59/9FGf2PYT3pwMpnEbNKysU+k/4FGwyGtMotOvcZOuW+EmXXYASRqYSXQfGL5cVIthOTgbkg== dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" -es6-iterator@^2.0.3, es6-iterator@~2.0.3: +es6-iterator@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= @@ -3886,7 +3884,7 @@ es6-iterator@^2.0.3, es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-symbol@^3.1.1, es6-symbol@~3.1.3: +es6-symbol@^3.1.1, es6-symbol@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== @@ -3954,7 +3952,7 @@ eslint-import-resolver-node@^0.3.6: debug "^3.2.7" resolve "^1.20.0" -eslint-module-utils@^2.7.2: +eslint-module-utils@^2.7.3: version "2.7.3" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== @@ -3963,23 +3961,23 @@ eslint-module-utils@^2.7.2: find-up "^2.1.0" eslint-plugin-import@^2.25.4: - version "2.25.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" - integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== + version "2.26.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" + integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== dependencies: array-includes "^3.1.4" array.prototype.flat "^1.2.5" debug "^2.6.9" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.2" + eslint-module-utils "^2.7.3" has "^1.0.3" - is-core-module "^2.8.0" + is-core-module "^2.8.1" is-glob "^4.0.3" - minimatch "^3.0.4" + minimatch "^3.1.2" object.values "^1.1.5" - resolve "^1.20.0" - tsconfig-paths "^3.12.0" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" eslint-plugin-jsx-a11y@^6.5.1: version "6.5.1" @@ -4005,26 +4003,26 @@ eslint-plugin-matrix-org@^0.4.0: integrity sha512-yVkNwtc33qtrQB4PPzpU+PUdFzdkENPan3JF4zhtAQJRUYXyvKEXnYSrXLUWYRXoYFxs9LbyI2CnhJL/RnHJaQ== eslint-plugin-react-hooks@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" - integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== + version "4.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz#71c39e528764c848d8253e1aa2c7024ed505f6c4" + integrity sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ== eslint-plugin-react@^7.28.0: - version "7.28.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf" - integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== + version "7.29.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz#4717de5227f55f3801a5fd51a16a4fa22b5914d2" + integrity sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ== dependencies: array-includes "^3.1.4" array.prototype.flatmap "^1.2.5" doctrine "^2.1.0" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.0.4" + minimatch "^3.1.2" object.entries "^1.1.5" object.fromentries "^2.0.5" object.hasown "^1.1.0" object.values "^1.1.5" - prop-types "^15.7.2" + prop-types "^15.8.1" resolve "^2.0.0-next.3" semver "^6.3.0" string.prototype.matchall "^4.0.6" @@ -4062,12 +4060,7 @@ eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1" - integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== - -eslint-visitor-keys@^3.3.0: +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== @@ -4589,9 +4582,9 @@ fragment-cache@^0.2.1: map-cache "^0.2.2" fs-extra@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" - integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -4627,7 +4620,7 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.0, function.prototype.name@^1.1.2, function.prototype.name@^1.1.4: +function.prototype.name@^1.1.0, function.prototype.name@^1.1.2, function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== @@ -4797,9 +4790,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.12.1" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" - integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== + version "13.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" + integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== dependencies: type-fest "^0.20.2" @@ -4827,16 +4820,11 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.2.4, graceful-fs@^4.2.9: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - grid-index@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/grid-index/-/grid-index-1.1.0.tgz#97f8221edec1026c8377b86446a7c71e79522ea7" @@ -4875,10 +4863,17 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== has-tostringtag@^1.0.0: version "1.0.0" @@ -4926,9 +4921,9 @@ has@^1.0.0, has@^1.0.1, has@^1.0.3: function-bind "^1.1.1" highlight.js@^11.3.1: - version "11.4.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.4.0.tgz#34ceadd49e1596ee5aba3d99346cdfd4845ee05a" - integrity sha512-nawlpCBCSASs7EdvZOYOYVkJpGmAOKMYZgZtUqSRqodZE0GRVcFKwo1RcpeOemqh9hyttTdd5wDBwHkuSyUfnA== + version "11.5.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.5.1.tgz#027c24e4509e2f4dcd00b4a6dda542ce0a1f7aea" + integrity sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q== hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" @@ -4975,9 +4970,9 @@ html-escaper@^2.0.0: integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== html-tags@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" - integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" + integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== htmlparser2@^3.10.0: version "3.10.1" @@ -5029,9 +5024,9 @@ http-signature@~1.3.6: sshpk "^1.14.1" https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" @@ -5065,11 +5060,6 @@ ieee754@^1.1.12, ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.1.8, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -5205,10 +5195,12 @@ is-arrow-function@^2.0.3: dependencies: is-callable "^1.0.4" -is-async-fn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-async-fn/-/is-async-fn-1.1.0.tgz#a1a15b11d4a1155cc23b11e91b301b45a3caad16" - integrity sha1-oaFbEdShFVzCOxHpGzAbRaPKrRY= +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" is-bigint@^1.0.1, is-bigint@^1.0.4: version "1.0.4" @@ -5261,7 +5253,7 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" -is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.0, is-core-module@^2.8.1: +is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== @@ -5356,7 +5348,7 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-finalizationregistry@^1.0.1: +is-finalizationregistry@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== @@ -5417,15 +5409,15 @@ is-map@^2.0.1, is-map@^2.0.2: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== -is-negative-zero@^2.0.1: +is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4, is-number-object@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== dependencies: has-tostringtag "^1.0.0" @@ -5496,10 +5488,12 @@ is-set@^2.0.1, is-set@^2.0.2: resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" @@ -5556,7 +5550,7 @@ is-weakmap@^2.0.1: resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== -is-weakref@^1.0.1: +is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== @@ -6259,12 +6253,10 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" +json5@^2.1.2, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== jsonfile@^6.0.1: version "6.1.0" @@ -6296,17 +6288,17 @@ jsprim@^2.0.2: verror "1.10.0" "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" - integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== + version "3.2.2" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.2.tgz#6ab1e52c71dfc0c0707008a91729a9491fe9f76c" + integrity sha512-HDAyJ4MNQBboGpUnHAVUNJs6X0lh058s6FuixsFGP7MgJYpD6Vasd6nzSG5iIfXu1zAYlHJ/zsOKNlrenTUBnw== dependencies: - array-includes "^3.1.3" + array-includes "^3.1.4" object.assign "^4.1.2" jszip@^3.7.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" - integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== + version "3.9.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.9.1.tgz#784e87f328450d1e8151003a9c67733e2b901051" + integrity sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -6604,9 +6596,9 @@ map-visit@^1.0.0: object-visit "^1.0.0" maplibre-gl@^1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-1.15.2.tgz#7fb47868b62455af916c090903f2154394450f9c" - integrity sha512-uPeV530apb4JfX3cRFfE+awFnbcJTOnCv2QvY4mw4huiInbybElWYkNzTs324YLSADq0f4bidRoYcR81ho3aLA== + version "1.15.3" + resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-1.15.3.tgz#eebbdd6b4cba46c61a660d6fa1808fced126e24d" + integrity sha512-ZuOhLCNgp7Yl1L9uyKgZeuo7kKdewP0iWtmEXsZ/snp0JiVkR1Kl+m1rsfKT/wpm/O4zZ7mUGxF16cYbMIFDRA== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" @@ -6622,7 +6614,7 @@ maplibre-gl@^1.15.2: geojson-vt "^3.2.1" gl-matrix "^3.2.1" grid-index "^1.1.0" - minimist "^1.2.5" + minimist "^1.2.6" murmurhash-js "^1.0.0" pbf "^3.2.1" potpack "^1.0.1" @@ -6653,7 +6645,7 @@ matrix-events-sdk@^0.0.1-beta.7: "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "17.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b58d09aa9a7a3578c43185becb41ab0b17ce0f98" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/db58a66e19608683682a780c777c606af68bc205" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -6805,24 +6797,24 @@ micromatch@^3.1.4: to-regex "^3.0.2" micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.2" + picomatch "^2.3.1" -mime-db@1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.51.0" + mime-db "1.52.0" mimic-fn@^2.1.0: version "2.1.0" @@ -6834,10 +6826,10 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.4: - version "3.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" - integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -6864,9 +6856,9 @@ mixin-deep@^1.2.0: is-extendable "^1.0.1" moo-color@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.2.tgz#837c40758d2d58763825d1359a84e330531eca64" - integrity sha512-5iXz5n9LWQzx/C2WesGFfpE6RLamzdHwsn3KpfzShwbfIqs7stnoEpaNErf/7+3mbxwZ4s8Foq7I0tPxw7BWHg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74" + integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ== dependencies: color-name "^1.1.4" @@ -6895,10 +6887,10 @@ murmurhash-js@^1.0.0: resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" integrity sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E= -nanoid@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== +nanoid@^3.3.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== nanomatch@^1.2.9: version "1.2.13" @@ -6937,11 +6929,6 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -6967,10 +6954,10 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-releases@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" - integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== +node-releases@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" + integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw== normalize-package-data@^2.5.0: version "2.5.0" @@ -7064,7 +7051,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.1.0, object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.7.0, object-inspect@^1.9.0: +object-inspect@^1.1.0, object-inspect@^1.12.0, object-inspect@^1.7.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== @@ -7077,7 +7064,7 @@ object-is@^1.0.2, object-is@^1.1.2: call-bind "^1.0.2" define-properties "^1.1.3" -object-keys@^1.0.12, object-keys@^1.0.9, object-keys@^1.1.1: +object-keys@^1.0.9, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -7388,7 +7375,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -7491,9 +7478,9 @@ postcss-scss@^2.1.1: postcss "^7.0.6" postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.5: - version "6.0.9" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" - integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -7517,11 +7504,11 @@ postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0. source-map "^0.6.1" postcss@^8.3.11: - version "8.4.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" - integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== + version "8.4.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" + integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== dependencies: - nanoid "^3.2.0" + nanoid "^3.3.1" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -7577,9 +7564,9 @@ process-nextick-args@~2.0.0: integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== promise-polyfill@^8.1.3: - version "8.2.1" - resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.1.tgz#1fa955b325bee4f6b8a4311e18148d4e5b46d254" - integrity sha512-3p9zj0cEHbp7NVUxEYUWjQlffXqnXaZIMPkAO7HhFh8u5636xLRDHOUo2vpWSK0T2mqm6fKLXYn1KP6PAZ2gKg== + version "8.2.3" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6" + integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg== promise@^7.0.3, promise@^7.1.1: version "7.3.1" @@ -7596,7 +7583,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7638,17 +7625,17 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pvtsutils@^1.2.0, pvtsutils@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.2.1.tgz#8212e846ca9afb21e40cebb0691755649f9f498a" - integrity sha512-Q867jEr30lBR2YSFFLZ0/XsEvpweqH6Kj096wmlRAFXrdRGPCNq2iz9B5Tk085EZ+OBZyYAVA5UhPkjSHGrUzQ== +pvtsutils@^1.2.1, pvtsutils@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.2.2.tgz#62ef6bc0513cbc255ee02574dedeaa41272d6101" + integrity sha512-OALo5ZEdqiI127i64+CXwkCOyFHUA+tCQgaUO/MvRDFXWPr53f2sx28ECNztUEzuyu5xvuuD1EB/szg9mwJoGA== dependencies: tslib "^2.3.1" pvutils@latest: - version "1.0.17" - resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.0.17.tgz#ade3c74dfe7178944fe44806626bd2e249d996bf" - integrity sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ== + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== qrcode@1.4.4: version "1.4.4" @@ -7729,9 +7716,9 @@ raw-loader@^4.0.2: schema-utils "^3.0.0" re-resizable@^6.9.0: - version "6.9.1" - resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.1.tgz#6be082b55d02364ca4bfee139e04feebdf52441c" - integrity sha512-KRYAgr9/j1PJ3K+t+MBhlQ+qkkoLDJ1rs0z1heIWvYbCW/9Vq4djDU+QumJ3hQbwwtzXF6OInla6rOx6hhgRhQ== + version "6.9.6" + resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.6.tgz#b95d37e3821481b56ddfb1e12862940a791e827d" + integrity sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ== dependencies: fast-memoize "^2.5.1" @@ -7786,15 +7773,20 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.0.0.tgz#026f6c4a27dbe33bf4a35655b9e1327c4e55e3f5" + integrity sha512-yUcBYdBBbo3QiPsgYDcfQcIkGZHfxOaoE6HLSnr1sPzMhdyxusbfKOSUbSd/ocGi32dxcj366PsTj+5oggeKKw== + +react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-redux@^7.2.0: - version "7.2.6" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" - integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== + version "7.2.8" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" + integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw== dependencies: "@babel/runtime" "^7.15.4" "@types/react-redux" "^7.1.20" @@ -7804,12 +7796,12 @@ react-redux@^7.2.0: react-is "^17.0.2" react-shallow-renderer@^16.13.1: - version "16.14.1" - resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" - integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== + version "16.15.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== dependencies: object-assign "^4.1.1" - react-is "^16.12.0 || ^17.0.0" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" react-test-renderer@^17.0.0, react-test-renderer@^17.0.2: version "17.0.2" @@ -7896,9 +7888,9 @@ redent@^3.0.0: strip-indent "^3.0.0" redux@^4.0.0, redux@^4.0.4: - version "4.1.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" - integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== dependencies: "@babel/runtime" "^7.9.2" @@ -7930,10 +7922,10 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== +regenerator-transform@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" + integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== dependencies: "@babel/runtime" "^7.8.4" @@ -7945,13 +7937,14 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.3.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" - integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== +regexp.prototype.flags@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" + functions-have-names "^1.2.2" regexpp@^3.2.0: version "3.2.0" @@ -8102,7 +8095,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0: +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== @@ -8285,9 +8278,9 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" @@ -8573,17 +8566,17 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2 strip-ansi "^6.0.1" string.prototype.matchall@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" - integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== + version "4.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" + integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" es-abstract "^1.19.1" get-intrinsic "^1.1.1" - has-symbols "^1.0.2" + has-symbols "^1.0.3" internal-slot "^1.0.3" - regexp.prototype.flags "^1.3.1" + regexp.prototype.flags "^1.4.1" side-channel "^1.0.4" string.prototype.repeat@^0.2.0: @@ -8766,9 +8759,9 @@ sugarss@^2.0.0: postcss "^7.0.2" supercluster@^7.1.0: - version "7.1.4" - resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.4.tgz#6762aabfd985d3390b49f13b815567d5116a828a" - integrity sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g== + version "7.1.5" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3" + integrity sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg== dependencies: kdbush "^3.0.0" @@ -8980,14 +8973,14 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -tsconfig-paths@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" - integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" - minimist "^1.2.0" + minimist "^1.2.6" strip-bom "^3.0.0" tslib@^1.8.1, tslib@^1.9.3: @@ -8995,7 +8988,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -9223,9 +9216,11 @@ url@^0.11.0: querystring "0.2.0" use-callback-ref@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" - integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + dependencies: + tslib "^2.0.0" use-memo-one@^1.1.1: version "1.1.2" @@ -9233,12 +9228,12 @@ use-memo-one@^1.1.1: integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== use-sidecar@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.5.tgz#ffff2a17c1df42e348624b699ba6e5c220527f2b" - integrity sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== dependencies: detect-node-es "^1.1.0" - tslib "^1.9.3" + tslib "^2.0.0" use@^3.1.0: version "3.1.1" @@ -9346,15 +9341,15 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" -webcrypto-core@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.4.0.tgz#9a395920792bcfa4605dc64aaf264156f79e873e" - integrity sha512-HY3Zo0GcRIQUUDnlZ/shGjN+4f7LVMkdJZoGPog+oHhJsJdMz6iM8Za5xZ0t6qg7Fx/JXXz+oBv2J2p982hGTQ== +webcrypto-core@^1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.3.tgz#58252c182c5fa88fd6b607b02b5246d087dbc6e3" + integrity sha512-8TnMtwwC/hQOyvElAOJ26lJKGgcErUG02KnKS1+QhjV4mDvQetVWU1EUEeLF8ICOrdc42+GypocyBJKRqo2kQg== dependencies: - "@peculiar/asn1-schema" "^2.0.44" + "@peculiar/asn1-schema" "^2.1.0" "@peculiar/json-schema" "^1.1.12" - asn1js "^2.1.1" - pvtsutils "^1.2.0" + asn1js "^2.3.2" + pvtsutils "^1.2.2" tslib "^2.3.1" webidl-conversions@^3.0.0: @@ -9428,22 +9423,22 @@ which-boxed-primitive@^1.0.2: is-symbol "^1.0.3" which-builtin-type@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.1.tgz#1d14bb1b69b5680ebdddd7244689574678a1d83c" - integrity sha512-zY3bUNzl/unBfSDS6ePT+/dwu6hZ7RMVMqHFvYxZEhisGEwCV/pYnXQ70nd3Hn2X6l8BNOWge5sHk3wAR3L42w== + version "1.1.2" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.2.tgz#254a34f6cd2a546e04d51d9a4ac2c65e9ed31bf4" + integrity sha512-2/+MF0XNPySHrIPlIAUB1dmQuWOPfQDR+TvwZs2tayroIA61MvZDJtkvwjv2iDg7h668jocdWsPOQwwAz5QUSg== dependencies: - function.prototype.name "^1.1.4" + function.prototype.name "^1.1.5" has-tostringtag "^1.0.0" - is-async-fn "^1.1.0" + is-async-function "^2.0.0" is-date-object "^1.0.5" - is-finalizationregistry "^1.0.1" + is-finalizationregistry "^1.0.2" is-generator-function "^1.0.10" is-regex "^1.1.4" - is-weakref "^1.0.1" + is-weakref "^1.0.2" isarray "^2.0.5" which-boxed-primitive "^1.0.2" which-collection "^1.0.1" - which-typed-array "^1.1.5" + which-typed-array "^1.1.7" which-collection@^1.0.1: version "1.0.1" @@ -9460,7 +9455,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-typed-array@^1.1.5: +which-typed-array@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.7.tgz#2761799b9a22d4b8660b3c1b40abaa7739691793" integrity sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw== @@ -9582,9 +9577,9 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3: integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== yargs-parser@^21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" - integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== yargs@^13.2.4: version "13.3.2" @@ -9616,9 +9611,9 @@ yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.0.1: - version "17.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" - integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + version "17.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284" + integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g== dependencies: cliui "^7.0.2" escalade "^3.1.1" From d5e911d8766239b1fd3790091af2c0ea3cd409e6 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 21 Apr 2022 11:37:38 +0200 Subject: [PATCH 035/102] Improve text of account deactivation dialog (#8371) * Improve text of account deactivation dialog * Reduce spacing of deactivate account dialog --- .../dialogs/_DeactivateAccountDialog.scss | 4 -- .../views/dialogs/DeactivateAccountDialog.tsx | 46 +++++-------------- src/i18n/strings/en_EN.json | 14 ++++-- 3 files changed, 20 insertions(+), 44 deletions(-) diff --git a/res/css/views/dialogs/_DeactivateAccountDialog.scss b/res/css/views/dialogs/_DeactivateAccountDialog.scss index 192917b2d00..8941afaf39c 100644 --- a/res/css/views/dialogs/_DeactivateAccountDialog.scss +++ b/res/css/views/dialogs/_DeactivateAccountDialog.scss @@ -18,10 +18,6 @@ limitations under the License. margin-bottom: 30px; } -.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section { - margin-top: 60px; -} - .mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section .mx_Field { width: 300px; } diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index a505d5e647d..dbdc3b3639a 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -86,7 +86,7 @@ export default class DeactivateAccountDialog extends React.Component
    -

    { _t( - "This will make your account permanently unusable. " + - "You will not be able to log in, and no one will be able to re-register the same " + - "user ID. " + - "This will cause your account to leave all rooms it is participating in, and it " + - "will remove your account details from your identity server. " + - "This action is irreversible.", - {}, - { b: (sub) => { sub } }, - ) }

    - -

    { _t( - "Deactivating your account does not by default cause us to forget messages you " + - "have sent. " + - "If you would like us to forget your messages, please tick the box below.", - {}, - { b: (sub) => { sub } }, - ) }

    - -

    { _t( - "Message visibility in Matrix is similar to email. " + - "Our forgetting your messages means that messages you have sent will not be shared " + - "with any new or unregistered users, but registered users who already have access " + - "to these messages will still have access to their copy.", - ) }

    +

    { _t("Confirm that you would like to deactivate your account. If you proceed:") }

    +
      +
    • { _t("You will not be able to reactivate your account") }
    • +
    • { _t("You will no longer be able to log in") }
    • +
    • { _t("No one will be able to reuse your username (MXID), including you: this username will remain unavailable") }
    • +
    • { _t("You will leave all rooms and DMs that you are in") }
    • +
    • { _t("You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number") }
    • +
    +

    { _t("Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?") }

    @@ -238,20 +222,12 @@ export default class DeactivateAccountDialog extends React.Component - { _t( - "Please forget all messages I have sent when my account is deactivated " + - "(Warning: this will cause future users to see an incomplete view " + - "of conversations)", - {}, - { b: (sub) => { sub } }, - ) } + { _t("Hide my messages from new joiners") }

    - { error } { auth }
    -
    ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dcf6772ce80..5204401e674 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2479,14 +2479,18 @@ "Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirm your account deactivation by using Single Sign On to prove your identity.", "Are you sure you want to deactivate your account? This is irreversible.": "Are you sure you want to deactivate your account? This is irreversible.", "Confirm account deactivation": "Confirm account deactivation", - "To continue, please enter your password:": "To continue, please enter your password:", + "To continue, please enter your account password:": "To continue, please enter your account password:", "There was a problem communicating with the server. Please try again.": "There was a problem communicating with the server. Please try again.", "Server did not require any authentication": "Server did not require any authentication", "Server did not return valid authentication information.": "Server did not return valid authentication information.", - "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.", - "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.", - "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.", - "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)", + "Confirm that you would like to deactivate your account. If you proceed:": "Confirm that you would like to deactivate your account. If you proceed:", + "You will not be able to reactivate your account": "You will not be able to reactivate your account", + "You will no longer be able to log in": "You will no longer be able to log in", + "No one will be able to reuse your username (MXID), including you: this username will remain unavailable": "No one will be able to reuse your username (MXID), including you: this username will remain unavailable", + "You will leave all rooms and DMs that you are in": "You will leave all rooms and DMs that you are in", + "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number": "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number", + "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?": "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?", + "Hide my messages from new joiners": "Hide my messages from new joiners", "Room": "Room", "Send custom timeline event": "Send custom timeline event", "Explore room state": "Explore room state", From 5d6143aaa73dd4cfbd28de21e9ee28795cb65fc9 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 21 Apr 2022 10:56:02 +0100 Subject: [PATCH 036/102] Extract view/join room logic to room helper (#8329) --- src/components/structures/RoomDirectory.tsx | 137 ++++------------ .../views/directory/NetworkDropdown.tsx | 7 +- src/i18n/strings/en_EN.json | 14 +- src/utils/DirectoryUtils.ts | 9 +- src/utils/error.ts | 24 +++ src/utils/rooms.ts | 155 ++++++++++++++++++ 6 files changed, 225 insertions(+), 121 deletions(-) create mode 100644 src/utils/error.ts diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 5577bc29e70..99aeb6f5478 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from "react"; -import { IFieldType, IInstance, IProtocol, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client"; +import { IFieldType, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client"; import { Visibility } from "matrix-js-sdk/src/@types/partials"; import { IRoomDirectoryOptions } from "matrix-js-sdk/src/@types/requests"; import { logger } from "matrix-js-sdk/src/logger"; @@ -28,7 +28,7 @@ import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; -import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown"; +import NetworkDropdown from "../views/directory/NetworkDropdown"; import SettingsStore from "../../settings/SettingsStore"; import { IDialogProps } from "../views/dialogs/IDialogProps"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; @@ -39,10 +39,11 @@ import DirectorySearchBox from "../views/elements/DirectorySearchBox"; import ScrollPanel from "./ScrollPanel"; import Spinner from "../views/elements/Spinner"; import { getDisplayAliasForAliasSet } from "../../Rooms"; -import { Action } from "../../dispatcher/actions"; import PosthogTrackers from "../../PosthogTrackers"; -import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { PublicRoomTile } from "../views/rooms/PublicRoomTile"; +import { getFieldsForThirdPartyLocation, joinRoomByAlias, showRoom } from "../../utils/rooms"; +import { GenericError } from "../../utils/error"; +import { ALL_ROOMS, Protocols } from "../../utils/DirectoryUtils"; const LAST_SERVER_KEY = "mx_last_room_directory_server"; const LAST_INSTANCE_KEY = "mx_last_room_directory_instance"; @@ -350,44 +351,23 @@ export default class RoomDirectory extends React.Component { }; private onJoinFromSearchClick = (alias: string) => { - // If we don't have a particular instance id selected, just show that rooms alias - if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) { - // If the user specified an alias without a domain, add on whichever server is selected - // in the dropdown - if (alias.indexOf(':') == -1) { - alias = alias + ':' + this.state.roomServer; - } - this.showRoomAlias(alias, true); - } else { - // This is a 3rd party protocol. Let's see if we can join it - const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); - const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - const fields = protocolName - ? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) - : null; - if (!fields) { - const brand = SdkConfig.get().brand; - Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, { - title: _t('Unable to join network'), - description: _t('%(brand)s does not know how to join a room on this network', { brand }), + const cli = MatrixClientPeg.get(); + try { + joinRoomByAlias(cli, alias, { + instanceId: this.state.instanceId, + roomServer: this.state.roomServer, + protocols: this.protocols, + metricsTrigger: "RoomDirectory", + }); + } catch (e) { + if (e instanceof GenericError) { + Modal.createTrackedDialog(e.message, '', ErrorDialog, { + title: e.message, + description: e.description, }); - return; + } else { + throw e; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { - if (resp.length > 0 && resp[0].alias) { - this.showRoomAlias(resp[0].alias, true); - } else { - Modal.createTrackedDialog('Room not found', '', ErrorDialog, { - title: _t('Room not found'), - description: _t('Couldn\'t find a matching Matrix room'), - }); - } - }, (e) => { - Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, { - title: _t('Fetching third party location failed'), - description: _t('Unable to look up room ID from server'), - }); - }); } }; @@ -401,55 +381,18 @@ export default class RoomDirectory extends React.Component { PosthogTrackers.trackInteraction("WebRoomDirectoryCreateRoomButton", ev); }; - private showRoomAlias(alias: string, autoJoin = false) { - this.showRoom(null, alias, autoJoin); - } - - private showRoom = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => { + private onRoomClick = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => { this.onFinished(); - const payload: ViewRoomPayload = { - action: Action.ViewRoom, - auto_join: autoJoin, - should_peek: shouldPeek, + const cli = MatrixClientPeg.get(); + showRoom(cli, room, { + roomAlias, + autoJoin, + shouldPeek, + roomServer: this.state.roomServer, metricsTrigger: "RoomDirectory", - }; - if (room) { - // Don't let the user view a room they won't be able to either - // peek or join: fail earlier so they don't have to click back - // to the directory. - if (MatrixClientPeg.get().isGuest()) { - if (!room.world_readable && !room.guest_can_join) { - dis.dispatch({ action: 'require_registration' }); - return; - } - } - - if (!roomAlias) { - roomAlias = getDisplayAliasForRoom(room); - } - - payload.oob_data = { - avatarUrl: room.avatar_url, - // XXX: This logic is duplicated from the JS SDK which - // would normally decide what the name is. - name: room.name || roomAlias || _t('Unnamed room'), - }; - - if (this.state.roomServer) { - payload.via_servers = [this.state.roomServer]; - } - } - // It's not really possible to join Matrix rooms by ID because the HS has no way to know - // which servers to start querying. However, there's no other way to join rooms in - // this list without aliases at present, so if roomAlias isn't set here we have no - // choice but to supply the ID. - if (roomAlias) { - payload.room_alias = roomAlias; - } else { - payload.room_id = room.room_id; - } - dis.dispatch(payload); + }); }; + private stringLooksLikeId(s: string, fieldType: IFieldType) { let pat = /^#[^\s]+:[^\s]/; if (fieldType && fieldType.regexp) { @@ -459,27 +402,11 @@ export default class RoomDirectory extends React.Component { return pat.test(s); } - private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) { - // make an object with the fields specified by that protocol. We - // require that the values of all but the last field come from the - // instance. The last is the user input. - const requiredFields = protocol.location_fields; - if (!requiredFields) return null; - const fields = {}; - for (let i = 0; i < requiredFields.length - 1; ++i) { - const thisField = requiredFields[i]; - if (instance.fields[thisField] === undefined) return null; - fields[thisField] = instance.fields[thisField]; - } - fields[requiredFields[requiredFields.length - 1]] = userInput; - return fields; - } - private onFinished = () => { this.props.onFinished(false); }; - render() { + public render() { let content; if (this.state.error) { content = this.state.error; @@ -491,7 +418,7 @@ export default class RoomDirectory extends React.Component { , ); @@ -571,7 +498,7 @@ export default class RoomDirectory extends React.Component { let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType); if (protocolName) { const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - if (this.getFieldsForThirdPartyLocation( + if (getFieldsForThirdPartyLocation( this.state.filterString, this.protocols[protocolName], instance, diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index cc7a559db0f..5a9e4100e1d 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { useEffect, useState } from "react"; import { MatrixError } from "matrix-js-sdk/src/http-api"; -import { IProtocol } from "matrix-js-sdk/src/client"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { instanceForInstanceId } from '../../../utils/DirectoryUtils'; @@ -42,9 +41,7 @@ import UIStore from "../../../stores/UIStore"; import { compare } from "../../../utils/strings"; import { SnakedObject } from "../../../utils/SnakedObject"; import { IConfigOptions } from "../../../IConfigOptions"; - -// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage -export const ALL_ROOMS = "ALL_ROOMS"; +import { ALL_ROOMS, Protocols } from "../../../utils/DirectoryUtils"; const SETTING_NAME = "room_directory_servers"; @@ -85,8 +82,6 @@ const validServer = withValidation({ ], }); -export type Protocols = Record; - interface IProps { protocols: Protocols; selectedServerName: string; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5204401e674..a4889b74cdf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -733,6 +733,13 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", + "Unnamed room": "Unnamed room", + "Unable to join network": "Unable to join network", + "%(brand)s does not know how to join a room on this network": "%(brand)s does not know how to join a room on this network", + "Room not found": "Room not found", + "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", + "Fetching third party location failed": "Fetching third party location failed", + "Unable to look up room ID from server": "Unable to look up room ID from server", "Error upgrading room": "Error upgrading room", "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Invite to %(spaceName)s": "Invite to %(spaceName)s", @@ -1753,7 +1760,6 @@ "Idle": "Idle", "Offline": "Offline", "Unknown": "Unknown", - "Unnamed room": "Unnamed room", "Preview": "Preview", "View": "View", "Join": "Join", @@ -3054,12 +3060,6 @@ "remove %(name)s from the directory.": "remove %(name)s from the directory.", "delete the address.": "delete the address.", "The server may be unavailable or overloaded": "The server may be unavailable or overloaded", - "Unable to join network": "Unable to join network", - "%(brand)s does not know how to join a room on this network": "%(brand)s does not know how to join a room on this network", - "Room not found": "Room not found", - "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", - "Fetching third party location failed": "Fetching third party location failed", - "Unable to look up room ID from server": "Unable to look up room ID from server", "Create new room": "Create new room", "No results for \"%(query)s\"": "No results for \"%(query)s\"", "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.", diff --git a/src/utils/DirectoryUtils.ts b/src/utils/DirectoryUtils.ts index 89e719f320b..f8b9e858d12 100644 --- a/src/utils/DirectoryUtils.ts +++ b/src/utils/DirectoryUtils.ts @@ -1,5 +1,5 @@ /* -Copyright 2018 The Matrix.org Foundation C.I.C. +Copyright 2018, 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. @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IInstance } from "matrix-js-sdk/src/client"; +import { IInstance, IProtocol } from "matrix-js-sdk/src/client"; -import { Protocols } from "../components/views/directory/NetworkDropdown"; +// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage +export const ALL_ROOMS = "ALL_ROOMS"; + +export type Protocols = Record; // Find a protocol 'instance' with a given instance_id // in the supplied protocols dict diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 00000000000..8dec29e7f06 --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,24 @@ +/* +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. +*/ + +export class GenericError extends Error { + constructor( + public readonly message: string, + public readonly description?: string | undefined, + ) { + super(message); + } +} diff --git a/src/utils/rooms.ts b/src/utils/rooms.ts index bd6fcf9d971..d9aa310ea4d 100644 --- a/src/utils/rooms.ts +++ b/src/utils/rooms.ts @@ -14,7 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { IInstance, IProtocol, IPublicRoomsChunkRoom, MatrixClient } from "matrix-js-sdk/src/client"; +import { ViewRoom as ViewRoomEvent } from "matrix-analytics-events/types/typescript/ViewRoom"; + +import { Action } from "../dispatcher/actions"; +import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import { getE2EEWellKnown } from "./WellKnownUtils"; +import dis from "../dispatcher/dispatcher"; +import { getDisplayAliasForAliasSet } from "../Rooms"; +import { _t } from "../languageHandler"; +import { instanceForInstanceId, protocolNameForInstanceId } from "./DirectoryUtils"; +import SdkConfig from "../SdkConfig"; +import { GenericError } from "./error"; +import { ALL_ROOMS, Protocols } from "./DirectoryUtils"; export function privateShouldBeEncrypted(): boolean { const e2eeWellKnown = getE2EEWellKnown(); @@ -24,3 +36,146 @@ export function privateShouldBeEncrypted(): boolean { } return true; } + +interface IShowRoomOpts { + roomAlias?: string; + autoJoin?: boolean; + shouldPeek?: boolean; + roomServer?: string; + metricsTrigger: ViewRoomEvent["trigger"]; +} + +export const showRoom = ( + client: MatrixClient, + room: IPublicRoomsChunkRoom | null, + { + roomAlias, + autoJoin = false, + shouldPeek = false, + roomServer, + }: IShowRoomOpts, +): void => { + const payload: ViewRoomPayload = { + action: Action.ViewRoom, + auto_join: autoJoin, + should_peek: shouldPeek, + metricsTrigger: "RoomDirectory", + }; + if (room) { + // Don't let the user view a room they won't be able to either + // peek or join: fail earlier so they don't have to click back + // to the directory. + if (client.isGuest()) { + if (!room.world_readable && !room.guest_can_join) { + dis.dispatch({ action: 'require_registration' }); + return; + } + } + + if (!roomAlias) { + roomAlias = getDisplayAliasForAliasSet(room.canonical_alias, room.aliases); + } + + payload.oob_data = { + avatarUrl: room.avatar_url, + // XXX: This logic is duplicated from the JS SDK which + // would normally decide what the name is. + name: room.name || roomAlias || _t('Unnamed room'), + }; + + if (roomServer) { + payload.via_servers = [roomServer]; + } + } + // It's not really possible to join Matrix rooms by ID because the HS has no way to know + // which servers to start querying. However, there's no other way to join rooms in + // this list without aliases at present, so if roomAlias isn't set here we have no + // choice but to supply the ID. + if (roomAlias) { + payload.room_alias = roomAlias; + } else { + payload.room_id = room.room_id; + } + dis.dispatch(payload); +}; + +interface IJoinRoomByAliasOpts { + instanceId?: string; + roomServer?: string; + protocols: Protocols; + metricsTrigger: ViewRoomEvent["trigger"]; +} + +export function joinRoomByAlias(cli: MatrixClient, alias: string, { + instanceId, + roomServer, + protocols, + metricsTrigger, +}: IJoinRoomByAliasOpts): void { + // If we don't have a particular instance id selected, just show that rooms alias + if (!instanceId || instanceId === ALL_ROOMS) { + // If the user specified an alias without a domain, add on whichever server is selected + // in the dropdown + if (!alias.includes(':')) { + alias = alias + ':' + roomServer; + } + showRoom(cli, null, { + roomAlias: alias, + autoJoin: true, + metricsTrigger, + }); + } else { + // This is a 3rd party protocol. Let's see if we can join it + const protocolName = protocolNameForInstanceId(protocols, instanceId); + const instance = instanceForInstanceId(protocols, instanceId); + const fields = protocolName + ? getFieldsForThirdPartyLocation(alias, protocols[protocolName], instance) + : null; + if (!fields) { + const brand = SdkConfig.get().brand; + throw new GenericError( + _t('Unable to join network'), + _t('%(brand)s does not know how to join a room on this network', { brand }), + ); + } + cli.getThirdpartyLocation(protocolName, fields).then((resp) => { + if (resp.length > 0 && resp[0].alias) { + showRoom(cli, null, { + roomAlias: resp[0].alias, + autoJoin: true, + metricsTrigger, + }); + } else { + throw new GenericError( + _t('Room not found'), + _t('Couldn\'t find a matching Matrix room'), + ); + } + }, (e) => { + throw new GenericError( + _t('Fetching third party location failed'), + _t('Unable to look up room ID from server'), + ); + }); + } +} + +export function getFieldsForThirdPartyLocation( + userInput: string, + protocol: IProtocol, + instance: IInstance, +): { searchFields?: string[] } | null { + // make an object with the fields specified by that protocol. We + // require that the values of all but the last field come from the + // instance. The last is the user input. + const requiredFields = protocol.location_fields; + if (!requiredFields) return null; + const fields = {}; + for (let i = 0; i < requiredFields.length - 1; ++i) { + const thisField = requiredFields[i]; + if (instance.fields[thisField] === undefined) return null; + fields[thisField] = instance.fields[thisField]; + } + fields[requiredFields[requiredFields.length - 1]] = userInput; + return fields; +} From c83ad1faa78ab818548f4775149269386a2f0a06 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 07:41:38 -0400 Subject: [PATCH 037/102] Add local echo of connected devices in video rooms (#8368) --- src/components/views/rooms/RoomTile.tsx | 22 +++++--- src/components/views/voip/VideoLobby.tsx | 10 ++-- src/utils/VideoChannelUtils.ts | 38 ++++++++++---- test/components/views/rooms/RoomTile-test.tsx | 51 +++++++++++++++---- .../components/views/voip/VideoLobby-test.tsx | 45 ++++++++++++---- test/test-utils/test-utils.ts | 1 + test/test-utils/video.ts | 2 +- 7 files changed, 126 insertions(+), 43 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 530b22571aa..7b0a8e95de9 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -81,7 +81,7 @@ interface IState { messagePreview?: string; videoStatus: VideoStatus; // Active video channel members, according to room state - videoMembers: RoomMember[]; + videoMembers: Set; // Active video channel members, according to Jitsi jitsiParticipants: IJitsiParticipant[]; } @@ -124,7 +124,7 @@ export default class RoomTile extends React.PureComponent { // generatePreview() will return nothing if the user has previews disabled messagePreview: "", videoStatus, - videoMembers: getConnectedMembers(this.props.room.currentState), + videoMembers: getConnectedMembers(this.props.room, videoStatus === VideoStatus.Connected), jitsiParticipants: VideoChannelStore.instance.participants, }; this.generatePreview(); @@ -593,7 +593,9 @@ export default class RoomTile extends React.PureComponent { } private updateVideoMembers = () => { - this.setState({ videoMembers: getConnectedMembers(this.props.room.currentState) }); + this.setState(state => ({ + videoMembers: getConnectedMembers(this.props.room, state.videoStatus === VideoStatus.Connected), + })); }; private updateVideoStatus = () => { @@ -610,7 +612,10 @@ export default class RoomTile extends React.PureComponent { private onConnectVideo = (roomId: string) => { if (roomId === this.props.room?.roomId) { - this.setState({ videoStatus: VideoStatus.Connected }); + this.setState({ + videoStatus: VideoStatus.Connected, + videoMembers: getConnectedMembers(this.props.room, true), + }); VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants); } }; @@ -623,7 +628,10 @@ export default class RoomTile extends React.PureComponent { private onDisconnectVideo = (roomId: string) => { if (roomId === this.props.room?.roomId) { - this.setState({ videoStatus: VideoStatus.Disconnected }); + this.setState({ + videoStatus: VideoStatus.Disconnected, + videoMembers: getConnectedMembers(this.props.room, false), + }); VideoChannelStore.instance.off(VideoChannelEvent.Participants, this.updateJitsiParticipants); } }; @@ -668,12 +676,12 @@ export default class RoomTile extends React.PureComponent { case VideoStatus.Disconnected: videoText = _t("Video"); videoActive = false; - participantCount = this.state.videoMembers.length; + participantCount = this.state.videoMembers.size; break; case VideoStatus.Connecting: videoText = _t("Connecting..."); videoActive = true; - participantCount = this.state.videoMembers.length; + participantCount = this.state.videoMembers.size; break; case VideoStatus.Connected: videoText = _t("Connected"); diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx index 84bc470273e..f9e95089270 100644 --- a/src/components/views/voip/VideoLobby.tsx +++ b/src/components/views/voip/VideoLobby.tsx @@ -110,7 +110,7 @@ const MAX_FACES = 8; const VideoLobby: FC<{ room: Room }> = ({ room }) => { const [connecting, setConnecting] = useState(false); const me = useMemo(() => room.getMember(room.myUserId), [room]); - const connectedMembers = useConnectedMembers(room.currentState); + const connectedMembers = useConnectedMembers(room, false); const videoRef = useRef(); const devices = useAsyncMemo(async () => { @@ -172,12 +172,12 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { }; let facePile; - if (connectedMembers.length) { - const shownMembers = connectedMembers.slice(0, MAX_FACES); - const overflow = connectedMembers.length > shownMembers.length; + if (connectedMembers.size) { + const shownMembers = [...connectedMembers].slice(0, MAX_FACES); + const overflow = connectedMembers.size > shownMembers.length; facePile =
    - { _t("%(count)s people connected", { count: connectedMembers.length }) } + { _t("%(count)s people connected", { count: connectedMembers.size }) }
    ; } diff --git a/src/utils/VideoChannelUtils.ts b/src/utils/VideoChannelUtils.ts index 11a1a9a35f2..cc3c99d980c 100644 --- a/src/utils/VideoChannelUtils.ts +++ b/src/utils/VideoChannelUtils.ts @@ -17,7 +17,8 @@ limitations under the License. import { useState } from "react"; import { throttle } from "lodash"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTypedEventEmitter } from "../hooks/useEventEmitter"; @@ -42,17 +43,32 @@ export const addVideoChannel = async (roomId: string, roomName: string) => { await WidgetUtils.addJitsiWidget(roomId, CallType.Video, "Video channel", VIDEO_CHANNEL, roomName); }; -export const getConnectedMembers = (state: RoomState): RoomMember[] => - state.getStateEvents(VIDEO_CHANNEL_MEMBER) +export const getConnectedMembers = (room: Room, connectedLocalEcho: boolean): Set => { + const members = new Set(); + + for (const e of room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER)) { + const member = room.getMember(e.getStateKey()); + let devices = e.getContent()?.devices ?? []; + + // Apply local echo for the disconnected case + if (!connectedLocalEcho && member?.userId === room.client.getUserId()) { + devices = devices.filter(d => d !== room.client.getDeviceId()); + } // Must have a device connected and still be joined to the room - .filter(e => e.getContent()?.devices?.length) - .map(e => state.getMember(e.getStateKey())) - .filter(member => member?.membership === "join"); - -export const useConnectedMembers = (state: RoomState, throttleMs = 100) => { - const [members, setMembers] = useState(getConnectedMembers(state)); - useTypedEventEmitter(state, RoomStateEvent.Update, throttle(() => { - setMembers(getConnectedMembers(state)); + if (devices.length && member?.membership === "join") members.add(member); + } + + // Apply local echo for the connected case + if (connectedLocalEcho) members.add(room.getMember(room.client.getUserId())); + return members; +}; + +export const useConnectedMembers = ( + room: Room, connectedLocalEcho: boolean, throttleMs = 100, +): Set => { + const [members, setMembers] = useState>(getConnectedMembers(room, connectedLocalEcho)); + useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => { + setMembers(getConnectedMembers(room, connectedLocalEcho)); }, throttleMs, { leading: true, trailing: true })); return members; }; diff --git a/test/components/views/rooms/RoomTile-test.tsx b/test/components/views/rooms/RoomTile-test.tsx index d209c32f0f9..d07360f6d4f 100644 --- a/test/components/views/rooms/RoomTile-test.tsx +++ b/test/components/views/rooms/RoomTile-test.tsx @@ -18,6 +18,8 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { @@ -26,6 +28,7 @@ import { mkRoom, mkVideoChannelMember, stubVideoChannelStore, + StubVideoChannelStore, } from "../../../test-utils"; import RoomTile from "../../../../src/components/views/rooms/RoomTile"; import SettingsStore from "../../../../src/settings/SettingsStore"; @@ -39,9 +42,8 @@ describe("RoomTile", () => { jest.spyOn(PlatformPeg, 'get') .mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform); - let cli; - let store; - + let cli: MatrixClient; + let store: StubVideoChannelStore; beforeEach(() => { const realGetValue = SettingsStore.getValue; SettingsStore.getValue = (name: string, roomId?: string): T => { @@ -52,7 +54,7 @@ describe("RoomTile", () => { }; stubClient(); - cli = mocked(MatrixClientPeg.get()); + cli = MatrixClientPeg.get(); store = stubVideoChannelStore(); DMRoomMap.makeShared(); }); @@ -60,8 +62,11 @@ describe("RoomTile", () => { afterEach(() => jest.clearAllMocks()); describe("video rooms", () => { - const room = mkRoom(cli, "!1:example.org"); - room.isElementVideoRoom.mockReturnValue(true); + let room: Room; + beforeEach(() => { + room = mkRoom(cli, "!1:example.org"); + mocked(room.isElementVideoRoom).mockReturnValue(true); + }); it("tracks connection state", () => { const tile = mount( @@ -97,7 +102,7 @@ describe("RoomTile", () => { mkVideoChannelMember("@chris:example.org", ["device 1"]), ])); - mocked(room.currentState).getMember.mockImplementation(userId => ({ + mocked(room).getMember.mockImplementation(userId => ({ userId, membership: userId === "@chris:example.org" ? "leave" : "join", name: userId, @@ -117,8 +122,36 @@ describe("RoomTile", () => { ); // Only Alice should display as connected - const participants = tile.find(".mx_RoomTile_videoParticipants"); - expect(participants.text()).toEqual("1"); + expect(tile.find(".mx_RoomTile_videoParticipants").text()).toEqual("1"); + }); + + it("reflects local echo in connected members", () => { + mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([ + // Make the remote echo claim that we're connected, while leaving the store disconnected + mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]), + ])); + + mocked(room).getMember.mockImplementation(userId => ({ + userId, + membership: "join", + name: userId, + rawDisplayName: userId, + roomId: "!1:example.org", + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + }) as unknown as RoomMember); + + const tile = mount( + , + ); + + // Because of our local echo, we should still appear as disconnected + expect(tile.find(".mx_RoomTile_videoParticipants").exists()).toEqual(false); }); }); }); diff --git a/test/components/views/voip/VideoLobby-test.tsx b/test/components/views/voip/VideoLobby-test.tsx index 4e7afb12c44..2d69709dc76 100644 --- a/test/components/views/voip/VideoLobby-test.tsx +++ b/test/components/views/voip/VideoLobby-test.tsx @@ -18,11 +18,14 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { stubClient, stubVideoChannelStore, + StubVideoChannelStore, mkRoom, mkVideoChannelMember, mockStateEventImplementation, @@ -33,7 +36,6 @@ import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar" import VideoLobby from "../../../../src/components/views/voip/VideoLobby"; describe("VideoLobby", () => { - stubClient(); Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: jest.fn(), @@ -42,19 +44,17 @@ describe("VideoLobby", () => { }); jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {}); - const cli = MatrixClientPeg.get(); - const room = mkRoom(cli, "!1:example.org"); - - let store; + let cli: MatrixClient; + let store: StubVideoChannelStore; + let room: Room; beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.get(); store = stubVideoChannelStore(); + room = mkRoom(cli, "!1:example.org"); mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]); }); - afterEach(() => { - jest.clearAllMocks(); - }); - describe("connected members", () => { it("hides when no one is connected", async () => { const lobby = mount(); @@ -75,7 +75,7 @@ describe("VideoLobby", () => { mkVideoChannelMember("@chris:example.org", ["device 1"]), ])); - mocked(room.currentState).getMember.mockImplementation(userId => ({ + mocked(room).getMember.mockImplementation(userId => ({ userId, membership: userId === "@chris:example.org" ? "leave" : "join", name: userId, @@ -95,6 +95,31 @@ describe("VideoLobby", () => { expect(memberText).toEqual("1 person connected"); expect(lobby.find(FacePile).find(MemberAvatar).props().member.userId).toEqual("@alice:example.org"); }); + + it("doesn't include remote echo of this device being connected", async () => { + mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([ + // Make the remote echo claim that we're connected, while leaving the store disconnected + mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]), + ])); + + mocked(room).getMember.mockImplementation(userId => ({ + userId, + membership: "join", + name: userId, + rawDisplayName: userId, + roomId: "!1:example.org", + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + }) as unknown as RoomMember); + + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + // Because of our local echo, we should still appear as disconnected + expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false); + }); }); describe("device buttons", () => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index fc85a825f31..a590474ffed 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -379,6 +379,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl getJoinRule: jest.fn().mockReturnValue("invite"), loadMembersIfNeeded: jest.fn(), client, + myUserId: client?.getUserId(), canInvite: jest.fn(), } as unknown as Room; } diff --git a/test/test-utils/video.ts b/test/test-utils/video.ts index 79c657a0c60..77fdfb8fcc0 100644 --- a/test/test-utils/video.ts +++ b/test/test-utils/video.ts @@ -21,7 +21,7 @@ import { mkEvent } from "./test-utils"; import { VIDEO_CHANNEL_MEMBER } from "../../src/utils/VideoChannelUtils"; import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../src/stores/VideoChannelStore"; -class StubVideoChannelStore extends EventEmitter { +export class StubVideoChannelStore extends EventEmitter { private _roomId: string; public get roomId(): string { return this._roomId; } private _connected: boolean; From dd880df6ae07300e77ad0fef96bb0378ac6ea09d Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 07:41:58 -0400 Subject: [PATCH 038/102] Forcefully disconnect from video rooms on logout and tab close (#8375) * Forcefully disconnect from video rooms on logout * Forcefully disconnect from video rooms on tab close --- src/components/structures/MatrixChat.tsx | 2 ++ src/stores/VideoChannelStore.ts | 41 +++++++++++++----------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 624909db31d..328af853142 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -131,6 +131,7 @@ import { IConfigOptions } from "../../IConfigOptions"; import { SnakedObject } from "../../utils/SnakedObject"; import InfoDialog from '../views/dialogs/InfoDialog'; import { leaveRoomBehaviour } from "../../utils/leave-behaviour"; +import VideoChannelStore from "../../stores/VideoChannelStore"; // legacy export export { default as Views } from "../../Views"; @@ -576,6 +577,7 @@ export default class MatrixChat extends React.PureComponent { break; case 'logout': CallHandler.instance.hangupAllCalls(); + if (VideoChannelStore.instance.connected) VideoChannelStore.instance.setDisconnected(); Lifecycle.logout(); break; case 'require_registration': diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 58e18cab985..14016800487 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -171,6 +171,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient { this.connected = true; messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + window.addEventListener("beforeunload", this.setDisconnected); this.emit(VideoChannelEvent.Connect, roomId); @@ -190,6 +191,27 @@ export default class VideoChannelStore extends AsyncStoreWithClient { } }; + public setDisconnected = async () => { + this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + window.removeEventListener("beforeunload", this.setDisconnected); + + const roomId = this.roomId; + this.activeChannel = null; + this.roomId = null; + this.connected = false; + this.participants = []; + + this.emit(VideoChannelEvent.Disconnect, roomId); + + // Tell others that we're disconnected, by removing our device from room state + await this.updateDevices(roomId, devices => { + const devicesSet = new Set(devices); + devicesSet.delete(this.matrixClient.getDeviceId()); + return Array.from(devicesSet); + }); + }; + private ack = (ev: CustomEvent) => { // Even if we don't have a reply to a given widget action, we still need // to give the widget API something to acknowledge receipt @@ -208,24 +230,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient { private onHangup = async (ev: CustomEvent) => { this.ack(ev); - - this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); - this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); - - const roomId = this.roomId; - this.activeChannel = null; - this.roomId = null; - this.connected = false; - this.participants = []; - - this.emit(VideoChannelEvent.Disconnect, roomId); - - // Tell others that we're disconnected, by removing our device from room state - await this.updateDevices(roomId, devices => { - const devicesSet = new Set(devices); - devicesSet.delete(this.matrixClient.getDeviceId()); - return Array.from(devicesSet); - }); + await this.setDisconnected(); }; private onParticipants = (ev: CustomEvent) => { From 146bcdd6a6314fef883a3569a9e95989f3857818 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 12:55:32 +0100 Subject: [PATCH 039/102] Move more stuff from BK to GHA (#8372) --- .editorconfig | 3 + .github/workflows/element-build-and-test.yaml | 126 +++++++++------ .github/workflows/end-to-end-tests.yaml | 99 ++++++------ .github/workflows/netlify.yaml | 145 +++++++++--------- .github/workflows/notify-element-web.yml | 27 ++-- .github/workflows/preview_changelog.yaml | 14 +- .github/workflows/static_analysis.yaml | 88 +++++++++++ .../{test_coverage.yml => tests.yml} | 17 +- .github/workflows/typecheck.yaml | 27 ---- 9 files changed, 336 insertions(+), 210 deletions(-) create mode 100644 .github/workflows/static_analysis.yaml rename .github/workflows/{test_coverage.yml => tests.yml} (78%) delete mode 100644 .github/workflows/typecheck.yaml diff --git a/.editorconfig b/.editorconfig index 880331a09e5..56631484cd5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,3 +21,6 @@ insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/element-build-and-test.yaml b/.github/workflows/element-build-and-test.yaml index 1633aae2609..905dbedb067 100644 --- a/.github/workflows/element-build-and-test.yaml +++ b/.github/workflows/element-build-and-test.yaml @@ -3,47 +3,87 @@ # as an artifact and run integration tests. name: Element Web - Build and Test on: - pull_request: + pull_request: { } + push: + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - build: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - steps: - - uses: actions/checkout@v2 - - name: Build - run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build - - name: Upload Artifact - uses: actions/upload-artifact@v2 - with: - name: previewbuild - path: element-web/webapp - # We'll only use this in a triggered job, then we're done with it - retention-days: 1 - cypress: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Download build - uses: actions/download-artifact@v3 - with: - name: previewbuild - path: webapp - - name: Run Cypress tests - uses: cypress-io/github-action@v2 - with: - # The built in Electron runner seems to grind to a halt trying - # to run the tests, so use chrome. - browser: chrome - start: npx serve -p 8080 webapp - - name: Upload Artifact - if: failure() - uses: actions/upload-artifact@v2 - with: - name: cypress-results - path: | - cypress/screenshots - cypress/videos - cypress/synapselogs + build: + name: "Build Element-Web" + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Fetch layered build + run: scripts/ci/layered.sh + + - name: Copy config + run: cp element.io/develop/config.json config.json + working-directory: ./element-web + + - name: Build + run: CI_PACKAGE=true yarn build + working-directory: ./element-web + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: previewbuild + path: element-web/webapp + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 + + cypress: + name: "Cypress End to End Tests" + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Download build + uses: actions/download-artifact@v3 + with: + name: previewbuild + path: webapp + + - name: Run Cypress tests + uses: cypress-io/github-action@v2 + with: + # The built in Electron runner seems to grind to a halt trying + # to run the tests, so use chrome. + browser: chrome + start: npx serve -p 8080 webapp + + - name: Upload Artifact + if: failure() + uses: actions/upload-artifact@v2 + with: + name: cypress-results + path: | + cypress/screenshots + cypress/videos + cypress/synapselogs + + app-tests: + name: Element Web Integration Tests + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Run tests + run: "./scripts/ci/app-tests.sh" diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 334af1772fd..1feaf266e36 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -1,47 +1,58 @@ name: End-to-end Tests on: - # These tests won't work for non-develop branches at the moment as they - # won't pull in the right versions of other repos, so they're only enabled - # on develop. - push: - branches: [develop] - pull_request: - branches: [develop] + # These tests won't work for non-develop branches at the moment as they + # won't pull in the right versions of other repos, so they're only enabled + # on develop. + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - end-to-end: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - container: vectorim/element-web-ci-e2etests-env:latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Prepare End-to-End tests - run: ./scripts/ci/prepare-end-to-end-tests.sh - - name: Run End-to-End tests - run: ./scripts/ci/run-end-to-end-tests.sh - - name: Archive logs - uses: actions/upload-artifact@v2 - if: ${{ always() }} - with: - path: | - test/end-to-end-tests/logs/**/* - test/end-to-end-tests/synapse/installations/consent/homeserver.log - retention-days: 14 - - name: Download previous benchmark data - uses: actions/cache@v1 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - name: Store benchmark result - uses: matrix-org/github-action-benchmark@jsperfentry-1 - with: - tool: 'jsperformanceentry' - output-file-path: test/end-to-end-tests/performance-entries.json - fail-on-alert: false - comment-on-alert: false - # Only temporary to monitor where failures occur - alert-comment-cc-users: '@gsouquet' - github-token: ${{ secrets.DEPLOY_GH_PAGES }} - auto-push: ${{ github.ref == 'refs/heads/develop' }} + end-to-end: + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + container: vectorim/element-web-ci-e2etests-env:latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh + + - name: Archive logs + uses: actions/upload-artifact@v2 + if: ${{ always() }} + with: + path: | + test/end-to-end-tests/logs/**/* + test/end-to-end-tests/synapse/installations/consent/homeserver.log + retention-days: 14 + + - name: Download previous benchmark data + uses: actions/cache@v1 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + + - name: Store benchmark result + uses: matrix-org/github-action-benchmark@jsperfentry-1 + with: + tool: 'jsperformanceentry' + output-file-path: test/end-to-end-tests/performance-entries.json + fail-on-alert: false + comment-on-alert: false + # Only temporary to monitor where failures occur + alert-comment-cc-users: '@gsouquet' + github-token: ${{ secrets.DEPLOY_GH_PAGES }} + auto-push: ${{ github.ref == 'refs/heads/develop' }} diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index ec09379b6e3..1acb7e8fd14 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -2,76 +2,79 @@ # and uploading it to netlify name: Upload Preview Build to Netlify on: - workflow_run: - workflows: ["Element Web - Build and Test"] - types: - - completed + workflow_run: + workflows: [ "Element Web - Build and Test" ] + types: + - completed jobs: - build: - runs-on: ubuntu-latest - if: > - ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} - steps: - - name: "🔍 Read PR number" - id: readctx - # we need to find the PR number that corresponds to the branch, which we do by - # searching the GH API - # The workflow_run event includes a list of pull requests, but it doesn't get populated for - # forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run - run: | - head_branch='${{github.event.workflow_run.head_repository.owner.login}}:${{github.event.workflow_run.head_branch}}' - echo "head branch: $head_branch" - pulls_uri="https://api.github.com/repos/${{ github.repository }}/pulls?head=$(jq -Rr '@uri' <<<$head_branch)" - pr_number=$(curl -s -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' "$pulls_uri" | - jq -r '.[] | .number') - echo "PR number: $pr_number" - echo "::set-output name=prnumber::$pr_number" - # There's a 'download artifact' action but it hasn't been updated for the - # workflow_run action (https://github.com/actions/download-artifact/issues/60) - # so instead we get this mess: - - name: 'Download artifact' - uses: actions/github-script@v3.1.0 - with: - script: | - var artifacts = await github.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{github.event.workflow_run.id }}, - }); - var matchArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "previewbuild" - })[0]; - var download = await github.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); - - name: Extract Artifacts - run: unzip -d webapp previewbuild.zip && rm previewbuild.zip - - name: Deploy to Netlify - id: netlify - uses: nwtgck/actions-netlify@v1.2 - with: - publish-dir: webapp - deploy-message: "Deploy from GitHub Actions" - # These don't work because we're in workflow_run - enable-pull-request-comment: false - enable-commit-comment: false - alias: pr${{ steps.readctx.outputs.prnumber }} - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - timeout-minutes: 1 - - name: Edit PR Description - uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - pull-request-number: ${{ steps.readctx.outputs.prnumber }} - description-message: | - Preview: ${{ steps.netlify.outputs.deploy-url }} - ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. + build: + runs-on: ubuntu-latest + if: > + ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + steps: + - name: "🔍 Read PR number" + id: readctx + # we need to find the PR number that corresponds to the branch, which we do by + # searching the GH API + # The workflow_run event includes a list of pull requests, but it doesn't get populated for + # forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run + run: | + head_branch='${{github.event.workflow_run.head_repository.owner.login}}:${{github.event.workflow_run.head_branch}}' + echo "head branch: $head_branch" + pulls_uri="https://api.github.com/repos/${{ github.repository }}/pulls?head=$(jq -Rr '@uri' <<<$head_branch)" + pr_number=$(curl -s -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' "$pulls_uri" | + jq -r '.[] | .number') + echo "PR number: $pr_number" + echo "::set-output name=prnumber::$pr_number" + # There's a 'download artifact' action but it hasn't been updated for the + # workflow_run action (https://github.com/actions/download-artifact/issues/60) + # so instead we get this mess: + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "previewbuild" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); + + - name: Extract Artifacts + run: unzip -d webapp previewbuild.zip && rm previewbuild.zip + + - name: Deploy to Netlify + id: netlify + uses: nwtgck/actions-netlify@v1.2 + with: + publish-dir: webapp + deploy-message: "Deploy from GitHub Actions" + # These don't work because we're in workflow_run + enable-pull-request-comment: false + enable-commit-comment: false + alias: pr${{ steps.readctx.outputs.prnumber }} + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + timeout-minutes: 1 + + - name: Edit PR Description + uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + pull-request-number: ${{ steps.readctx.outputs.prnumber }} + description-message: | + Preview: ${{ steps.netlify.outputs.deploy-url }} + ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index ef463784f38..c5c89905ced 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -1,15 +1,18 @@ name: Notify element-web on: - push: - branches: [develop] + push: + branches: [ develop ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - notify-element-web: - runs-on: ubuntu-latest - environment: develop - steps: - - name: Notify element-web repo that a new SDK build is on develop - uses: peter-evans/repository-dispatch@v1 - with: - token: ${{ secrets.ELEMENT_WEB_NOTIFY_TOKEN }} - repository: vector-im/element-web - event-type: element-web-notify + notify-element-web: + name: "Notify Element Web" + runs-on: ubuntu-latest + environment: develop + steps: + - name: Notify element-web repo that a new SDK build is on develop + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ secrets.ELEMENT_BOT_TOKEN }} + repository: vector-im/element-web + event-type: element-web-notify diff --git a/.github/workflows/preview_changelog.yaml b/.github/workflows/preview_changelog.yaml index d68d19361da..786d828d419 100644 --- a/.github/workflows/preview_changelog.yaml +++ b/.github/workflows/preview_changelog.yaml @@ -3,10 +3,10 @@ on: pull_request_target: types: [ opened, edited, labeled ] jobs: - changelog: - runs-on: ubuntu-latest - steps: - - name: Preview Changelog - uses: matrix-org/allchange@main - with: - ghToken: ${{ secrets.GITHUB_TOKEN }} + changelog: + runs-on: ubuntu-latest + steps: + - name: Preview Changelog + uses: matrix-org/allchange@main + with: + ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml new file mode 100644 index 00000000000..8e320d99920 --- /dev/null +++ b/.github/workflows/static_analysis.yaml @@ -0,0 +1,88 @@ +name: Static Analysis +on: + pull_request: { } + push: + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] +jobs: + ts_lint: + name: "Typescript Syntax Check" + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Install Deps + run: "./scripts/ci/install-deps.sh --ignore-scripts" + + - name: Typecheck + run: "yarn run lint:types" + + - name: Switch js-sdk to release mode + run: | + scripts/ci/js-sdk-to-release.js + cd node_modules/matrix-js-sdk + yarn install + yarn run build:compile + yarn run build:types + + - name: Typecheck (release mode) + run: "yarn run lint:types" + + i18n_lint: + name: "i18n Diff Check" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: i18n Check + run: "yarn run diff-i18n" + + js_lint: + name: "ESLint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: Run Linter + run: "yarn run lint:js" + + style_lint: + name: "Style Lint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: Run Linter + run: "yarn run lint:style" diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/tests.yml similarity index 78% rename from .github/workflows/test_coverage.yml rename to .github/workflows/tests.yml index 4cd9f6d2f06..dc11981b7cf 100644 --- a/.github/workflows/test_coverage.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,13 @@ -name: Test coverage +name: Tests on: - pull_request: {} + pull_request: { } push: - branches: [develop, main, master] + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - test-coverage: + jest: + name: Jest with Codecov runs-on: ubuntu-latest env: # This must be set for fetchdep.sh to get the right branch @@ -19,13 +22,15 @@ jobs: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} - name: Yarn cache - uses: c-hive/gha-yarn-cache@v2 + uses: actions/setup-node@v3 + with: + cache: 'yarn' - name: Install Deps run: "./scripts/ci/install-deps.sh --ignore-scripts" - name: Run tests with coverage - run: "yarn install && yarn coverage" + run: "yarn coverage" - name: Upload coverage uses: codecov/codecov-action@v2 diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/typecheck.yaml deleted file mode 100644 index 60cabb3caba..00000000000 --- a/.github/workflows/typecheck.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Type Check -on: - pull_request: - branches: [develop] -jobs: - build: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - steps: - - uses: actions/checkout@v2 - - uses: c-hive/gha-yarn-cache@v2 - - name: Install Deps - run: "./scripts/ci/install-deps.sh --ignore-scripts" - - name: Typecheck - run: "yarn run lint:types" - - name: Switch js-sdk to release mode - run: | - scripts/ci/js-sdk-to-release.js - cd node_modules/matrix-js-sdk - yarn install - yarn run build:compile - yarn run build:types - - name: Typecheck (release mode) - run: "yarn run lint:types" - From 6f5900557b888184b148cb2cf4c568762ded1032 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 15:17:43 +0100 Subject: [PATCH 040/102] Remove notify environment requirement (#8383) --- .github/workflows/notify-element-web.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index c5c89905ced..1d60a1523cc 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -8,7 +8,6 @@ jobs: notify-element-web: name: "Notify Element Web" runs-on: ubuntu-latest - environment: develop steps: - name: Notify element-web repo that a new SDK build is on develop uses: peter-evans/repository-dispatch@v1 From c70816d76322c62e21d96d2e914193a076fd9b3c Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 11:00:32 -0400 Subject: [PATCH 041/102] Persist audio and video mute state in video rooms (#8376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …so that video lobbies remember whether you've disabled your camera. --- src/components/views/voip/VideoLobby.tsx | 16 ++++++--- src/stores/VideoChannelStore.ts | 44 +++++++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx index f9e95089270..862e58fe630 100644 --- a/src/components/views/voip/VideoLobby.tsx +++ b/src/components/views/voip/VideoLobby.tsx @@ -21,7 +21,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../../languageHandler"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; -import { useStateToggle } from "../../../hooks/useStateToggle"; import { useConnectedMembers } from "../../../utils/VideoChannelUtils"; import VideoChannelStore from "../../../stores/VideoChannelStore"; import IconizedContextMenu, { @@ -108,6 +107,7 @@ const DeviceButton: FC = ({ const MAX_FACES = 8; const VideoLobby: FC<{ room: Room }> = ({ room }) => { + const store = VideoChannelStore.instance; const [connecting, setConnecting] = useState(false); const me = useMemo(() => room.getMember(room.myUserId), [room]); const connectedMembers = useConnectedMembers(room, false); @@ -130,8 +130,16 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { const audioDevice = selectedAudioDevice ?? audioDevices[0]; const videoDevice = selectedVideoDevice ?? videoDevices[0]; - const [audioActive, toggleAudio] = useStateToggle(true); - const [videoActive, toggleVideo] = useStateToggle(true); + const [audioActive, setAudioActive] = useState(!store.audioMuted); + const [videoActive, setVideoActive] = useState(!store.videoMuted); + const toggleAudio = () => { + store.audioMuted = audioActive; + setAudioActive(!audioActive); + }; + const toggleVideo = () => { + store.videoMuted = videoActive; + setVideoActive(!videoActive); + }; const videoStream = useAsyncMemo(async () => { if (videoDevice && videoActive) { @@ -162,7 +170,7 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { const connect = async () => { setConnecting(true); try { - await VideoChannelStore.instance.connect( + await store.connect( room.roomId, audioActive ? audioDevice : null, videoActive ? videoDevice : null, ); } catch (e) { diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 14016800487..d32f748fb7f 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -94,6 +94,20 @@ export default class VideoChannelStore extends AsyncStoreWithClient { public get participants(): IJitsiParticipant[] { return this._participants; } private set participants(value: IJitsiParticipant[]) { this._participants = value; } + private _audioMuted = localStorage.getItem("mx_audioMuted") === "true"; + public get audioMuted(): boolean { return this._audioMuted; } + public set audioMuted(value: boolean) { + this._audioMuted = value; + localStorage.setItem("mx_audioMuted", value.toString()); + } + + private _videoMuted = localStorage.getItem("mx_videoMuted") === "true"; + public get videoMuted(): boolean { return this._videoMuted; } + public set videoMuted(value: boolean) { + this._videoMuted = value; + localStorage.setItem("mx_videoMuted", value.toString()); + } + public connect = async (roomId: string, audioDevice: MediaDeviceInfo, videoDevice: MediaDeviceInfo) => { if (this.activeChannel) await this.disconnect(); @@ -136,10 +150,14 @@ export default class VideoChannelStore extends AsyncStoreWithClient { } } + // Participant data and mute state will come down the event pipeline quickly, so prepare in advance this.activeChannel = messaging; this.roomId = roomId; - // Participant data will come down the event pipeline quickly, so prepare in advance messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + messaging.on(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio); + messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio); + messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo); + messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo); this.emit(VideoChannelEvent.StartConnect, roomId); @@ -163,6 +181,10 @@ export default class VideoChannelStore extends AsyncStoreWithClient { this.activeChannel = null; this.roomId = null; messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio); + messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio); + messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo); + messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo); this.emit(VideoChannelEvent.Disconnect, roomId); @@ -238,4 +260,24 @@ export default class VideoChannelStore extends AsyncStoreWithClient { this.emit(VideoChannelEvent.Participants, this.roomId, ev.detail.data.participants); this.ack(ev); }; + + private onMuteAudio = (ev: CustomEvent) => { + this.audioMuted = true; + this.ack(ev); + }; + + private onUnmuteAudio = (ev: CustomEvent) => { + this.audioMuted = false; + this.ack(ev); + }; + + private onMuteVideo = (ev: CustomEvent) => { + this.videoMuted = true; + this.ack(ev); + }; + + private onUnmuteVideo = (ev: CustomEvent) => { + this.videoMuted = false; + this.ack(ev); + }; } From 73e8387799ba446a6da62e41c24390a5fbfa7b27 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 21 Apr 2022 18:14:10 +0200 Subject: [PATCH 042/102] test functional EventUtils (#8386) * extract poll event test util Signed-off-by: Kerry Archibald * test isContentActionable Signed-off-by: Kerry Archibald * test canEditContent Signed-off-by: Kerry Archibald * test functional eventutils Signed-off-by: Kerry Archibald * copyrights Signed-off-by: Kerry Archibald --- src/utils/EventUtils.ts | 2 +- .../previews/PollStartEventPreview-test.ts | 40 +- test/test-utils/index.ts | 1 + test/test-utils/poll.ts | 50 +++ test/utils/EventUtils-test.ts | 357 ++++++++++++++++++ 5 files changed, 413 insertions(+), 37 deletions(-) create mode 100644 test/test-utils/poll.ts create mode 100644 test/utils/EventUtils-test.ts diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 51ab6cbed06..0a860ccf013 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -82,7 +82,7 @@ export function canEditContent(mxEvent: MatrixEvent): boolean { M_POLL_START.matches(mxEvent.getType()) || ( (msgtype === MsgType.Text || msgtype === MsgType.Emote) && - body && + !!body && typeof body === 'string' ) ); diff --git a/test/stores/room-list/previews/PollStartEventPreview-test.ts b/test/stores/room-list/previews/PollStartEventPreview-test.ts index a7726167e11..b69e7da9767 100644 --- a/test/stores/room-list/previews/PollStartEventPreview-test.ts +++ b/test/stores/room-list/previews/PollStartEventPreview-test.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { POLL_ANSWER, M_TEXT, M_POLL_KIND_DISCLOSED, M_POLL_START } from "matrix-events-sdk"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { PollStartEventPreview } from "../../../../src/stores/room-list/previews/PollStartEventPreview"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { makePollStartEvent } from "../../../test-utils"; jest.spyOn(MatrixClientPeg, 'get').mockReturnValue({ getUserId: () => "@me:example.com", @@ -26,47 +26,15 @@ jest.spyOn(MatrixClientPeg, 'get').mockReturnValue({ describe("PollStartEventPreview", () => { it("shows the question for a poll I created", async () => { - const pollStartEvent = newPollStartEvent("My Question", "@me:example.com"); + const pollStartEvent = makePollStartEvent("My Question", "@me:example.com"); const preview = new PollStartEventPreview(); expect(preview.getTextFor(pollStartEvent)).toBe("My Question"); }); it("shows the sender and question for a poll created by someone else", async () => { - const pollStartEvent = newPollStartEvent("Your Question", "@yo:example.com"); + const pollStartEvent = makePollStartEvent("Your Question", "@yo:example.com"); const preview = new PollStartEventPreview(); expect(preview.getTextFor(pollStartEvent)).toBe("@yo:example.com: Your Question"); }); }); -function newPollStartEvent( - question: string, - sender: string, - answers?: POLL_ANSWER[], -): MatrixEvent { - if (!answers) { - answers = [ - { "id": "socks", [M_TEXT.name]: "Socks" }, - { "id": "shoes", [M_TEXT.name]: "Shoes" }, - ]; - } - - return new MatrixEvent( - { - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "sender": sender, - "type": M_POLL_START.name, - "content": { - [M_POLL_START.name]: { - "question": { - [M_TEXT.name]: question, - }, - "kind": M_POLL_KIND_DISCLOSED.name, - "answers": answers, - }, - [M_TEXT.name]: `${question}: answers`, - }, - }, - ); -} - diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index c886e6a798b..44ea28c9660 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -2,6 +2,7 @@ export * from './beacon'; export * from './client'; export * from './location'; export * from './platform'; +export * from './poll'; export * from './room'; export * from './test-utils'; export * from './video'; diff --git a/test/test-utils/poll.ts b/test/test-utils/poll.ts new file mode 100644 index 00000000000..ca25b9eaa0a --- /dev/null +++ b/test/test-utils/poll.ts @@ -0,0 +1,50 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { M_TEXT, M_POLL_START, POLL_ANSWER, M_POLL_KIND_DISCLOSED } from "matrix-events-sdk"; + +export const makePollStartEvent = ( + question: string, + sender: string, + answers?: POLL_ANSWER[], +): MatrixEvent => { + if (!answers) { + answers = [ + { "id": "socks", [M_TEXT.name]: "Socks" }, + { "id": "shoes", [M_TEXT.name]: "Shoes" }, + ]; + } + + return new MatrixEvent( + { + "event_id": "$mypoll", + "room_id": "#myroom:example.com", + "sender": sender, + "type": M_POLL_START.name, + "content": { + [M_POLL_START.name]: { + "question": { + [M_TEXT.name]: question, + }, + "kind": M_POLL_KIND_DISCLOSED.name, + "answers": answers, + }, + [M_TEXT.name]: `${question}: answers`, + }, + }, + ); +}; diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts new file mode 100644 index 00000000000..674162f548d --- /dev/null +++ b/test/utils/EventUtils-test.ts @@ -0,0 +1,357 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; +import { + EventStatus, + EventType, + MatrixEvent, + MsgType, + RelationType, +} from "matrix-js-sdk/src/matrix"; + +import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import { + canCancel, + canEditContent, + canEditOwnEvent, + canForward, + isContentActionable, + isLocationEvent, + isVoiceMessage, +} from "../../src/utils/EventUtils"; +import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent } from "../test-utils"; + +describe('EventUtils', () => { + const userId = '@user:server'; + const roomId = '!room:server'; + const mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue(userId), + }); + + beforeEach(() => { + mockClient.getUserId.mockClear().mockReturnValue(userId); + }); + afterAll(() => { + jest.spyOn(MatrixClientPeg, 'get').mockRestore(); + }); + + // setup events + const unsentEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + }); + unsentEvent.status = EventStatus.ENCRYPTING; + + const redactedEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + }); + redactedEvent.makeRedacted(redactedEvent); + + const stateEvent = makeBeaconInfoEvent(userId, roomId); + + const roomMemberEvent = new MatrixEvent({ + type: EventType.RoomMember, + sender: userId, + }); + + const stickerEvent = new MatrixEvent({ + type: EventType.Sticker, + sender: userId, + }); + + const pollStartEvent = makePollStartEvent('What?', userId); + + const notDecryptedEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: 'm.bad.encrypted', + }, + }); + + const noMsgType = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: undefined, + }, + }); + + const noContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Image, + }, + }); + + const emptyContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: '', + }, + }); + + const objectContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.File, + body: {}, + }, + }); + + const niceTextMessage = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + + const bobsTextMessage = new MatrixEvent({ + type: EventType.RoomMessage, + sender: '@bob:server', + content: { + msgtype: MsgType.Text, + body: 'Hello from Bob', + }, + }); + + describe('isContentActionable()', () => { + type TestCase = [string, MatrixEvent]; + it.each([ + ['unsent event', unsentEvent], + ['redacted event', redactedEvent], + ['state event', stateEvent], + ['undecrypted event', notDecryptedEvent], + ['room member event', roomMemberEvent], + ['event without msgtype', noMsgType], + ['event without content body property', noContentBody], + ])('returns false for %s', (_description, event) => { + expect(isContentActionable(event)).toBe(false); + }); + + it.each([ + ['sticker event', stickerEvent], + ['poll start event', pollStartEvent], + ['event with empty content body', emptyContentBody], + ['event with a content body', niceTextMessage], + ])('returns true for %s', (_description, event) => { + expect(isContentActionable(event)).toBe(true); + }); + }); + + describe('editable content helpers', () => { + const replaceRelationEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + ['m.relates_to']: { + rel_type: RelationType.Replace, + event_id: '1', + }, + }, + }); + + const referenceRelationEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + ['m.relates_to']: { + rel_type: RelationType.Reference, + event_id: '1', + }, + }, + }); + + const emoteEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Emote, + body: '🧪', + }, + }); + + type TestCase = [string, MatrixEvent]; + + const uneditableCases: TestCase[] = [ + ['redacted event', redactedEvent], + ['state event', stateEvent], + ['event that is not room message', roomMemberEvent], + ['event without msgtype', noMsgType], + ['event without content body property', noContentBody], + ['event with empty content body property', emptyContentBody], + ['event with non-string body', objectContentBody], + ['event not sent by current user', bobsTextMessage], + ['event with a replace relation', replaceRelationEvent], + ]; + + const editableCases: TestCase[] = [ + ['event with reference relation', referenceRelationEvent], + ['emote event', emoteEvent], + ['poll start event', pollStartEvent], + ['event with a content body', niceTextMessage], + ]; + + describe('canEditContent()', () => { + it.each(uneditableCases)('returns false for %s', (_description, event) => { + expect(canEditContent(event)).toBe(false); + }); + + it.each(editableCases)('returns true for %s', (_description, event) => { + expect(canEditContent(event)).toBe(true); + }); + }); + describe('canEditOwnContent()', () => { + it.each(uneditableCases)('returns false for %s', (_description, event) => { + expect(canEditOwnEvent(event)).toBe(false); + }); + + it.each(editableCases)('returns true for %s', (_description, event) => { + expect(canEditOwnEvent(event)).toBe(true); + }); + }); + }); + + describe('isVoiceMessage()', () => { + it('returns true for an event with msc2516.voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + ['org.matrix.msc2516.voice']: {}, + }, + }); + + expect(isVoiceMessage(event)).toBe(true); + }); + + it('returns true for an event with msc3245.voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + ['org.matrix.msc3245.voice']: {}, + }, + }); + + expect(isVoiceMessage(event)).toBe(true); + }); + + it('returns false for an event with voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'hello', + }, + }); + + expect(isVoiceMessage(event)).toBe(false); + }); + }); + + describe('isLocationEvent()', () => { + it('returns true for an event with m.location stable type', () => { + const event = new MatrixEvent({ + type: M_LOCATION.altName, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for an event with m.location unstable prefixed type', () => { + const event = new MatrixEvent({ + type: M_LOCATION.name, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for a room message with stable m.location msgtype', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + msgtype: M_LOCATION.altName, + }, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for a room message with unstable m.location msgtype', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + msgtype: M_LOCATION.name, + }, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns false for a non location event', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'Hello', + }, + }); + expect(isLocationEvent(event)).toBe(false); + }); + }); + + describe('canForward()', () => { + it('returns false for a location event', () => { + const event = new MatrixEvent({ + type: M_LOCATION.name, + }); + expect(canForward(event)).toBe(false); + }); + it('returns false for a poll event', () => { + const event = makePollStartEvent('Who?', userId); + expect(canForward(event)).toBe(false); + }); + it('returns true for a room message event', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'Hello', + }, + }); + expect(canForward(event)).toBe(true); + }); + }); + + describe('canCancel()', () => { + it.each([ + [EventStatus.QUEUED], + [EventStatus.NOT_SENT], + [EventStatus.ENCRYPTING], + ])('return true for status %s', (status) => { + expect(canCancel(status)).toBe(true); + }); + + it.each([ + [EventStatus.SENDING], + [EventStatus.CANCELLED], + [EventStatus.SENT], + ['invalid-status' as unknown as EventStatus], + ])('return false for status %s', (status) => { + expect(canCancel(status)).toBe(false); + }); + }); +}); From 86419b19255ac1af06da6a17666d29bf267750d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 17:21:44 +0100 Subject: [PATCH 043/102] Fix regression around the room list treeview keyboard a11y (#8385) --- src/components/views/rooms/RoomList.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index e2550bc2b6e..9251cfc2ba6 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -616,11 +616,8 @@ export default class RoomList extends React.PureComponent { public focus(): void { // focus the first focusable element in this aria treeview widget const treeItems = this.treeRef.current?.querySelectorAll('[role="treeitem"]'); - if (treeItems) { - return; - } - [...treeItems] - .find(e => e.offsetParent !== null)?.focus(); + if (!treeItems) return; + [...treeItems].find(e => e.offsetParent !== null)?.focus(); } public render() { From 399ac618c767d47f035d75a59d22b6db93459f10 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 21 Apr 2022 18:56:11 +0200 Subject: [PATCH 044/102] LLS: fix jumpy maximised map (#8387) * add maxzoom to map fit bounds Signed-off-by: Kerry Archibald * take snapshot of bounds at center on dialog open Signed-off-by: Kerry Archibald --- .../views/beacon/BeaconViewDialog.tsx | 18 ++++++++--- src/components/views/location/Map.tsx | 2 +- .../views/beacon/BeaconViewDialog-test.tsx | 30 +++++++++++++++++++ test/components/views/location/Map-test.tsx | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 9dc1352f105..e6c4a423fe9 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { Beacon, @@ -56,6 +56,17 @@ const getBoundsCenter = (bounds: Bounds): string | undefined => { }); }; +const useInitialMapPosition = (liveBeacons: Beacon[], focusBeacon?: Beacon): { + bounds?: Bounds; centerGeoUri: string; +} => { + const bounds = useRef(getBeaconBounds(liveBeacons)); + const centerGeoUri = useRef( + focusBeacon?.latestLocationState?.uri || + getBoundsCenter(bounds.current), + ); + return { bounds: bounds.current, centerGeoUri: centerGeoUri.current }; +}; + /** * Dialog to view live beacons maximised */ @@ -69,8 +80,7 @@ const BeaconViewDialog: React.FC = ({ const [isSidebarOpen, setSidebarOpen] = useState(false); - const bounds = getBeaconBounds(liveBeacons); - const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds); + const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon); return ( = ({ fixedWidth={false} > - { !!bounds ? [bounds.west, bounds.south], [bounds.east, bounds.north], ); - map.fitBounds(lngLatBounds, { padding: 100 }); + map.fitBounds(lngLatBounds, { padding: 100, maxZoom: 15 }); } catch (error) { logger.error('Invalid map bounds', error); } diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx index 0f23036b8b1..8d0fb30e307 100644 --- a/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -24,6 +24,7 @@ import { RoomMember, getBeaconInfoIdentifier, } from 'matrix-js-sdk/src/matrix'; +import maplibregl from 'maplibre-gl'; import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog'; import { @@ -58,6 +59,8 @@ describe('', () => { getVisibleRooms: jest.fn().mockReturnValue([]), }); + const mockMap = new maplibregl.Map(); + // make fresh rooms every time // as we update room state const setupRoom = (stateEvents: MatrixEvent[] = []): Room => { @@ -88,6 +91,8 @@ describe('', () => { beforeEach(() => { jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore(); + + jest.clearAllMocks(); }); it('renders a map with markers', () => { @@ -151,6 +156,31 @@ describe('', () => { expect(component.find('BeaconMarker').length).toEqual(2); }); + it('does not update bounds or center on changing beacons', () => { + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + expect(component.find('BeaconMarker').length).toEqual(1); + + const anotherBeaconEvent = makeBeaconInfoEvent(bobId, + roomId, + { isLive: true }, + '$bob-room1-1', + ); + + act(() => { + // emits RoomStateEvent.BeaconLiveness + room.currentState.setStateEvents([anotherBeaconEvent]); + }); + + component.setProps({}); + + // two markers now! + expect(mockMap.setCenter).toHaveBeenCalledTimes(1); + expect(mockMap.fitBounds).toHaveBeenCalledTimes(1); + }); + it('renders a fallback when no live beacons remain', () => { const onFinished = jest.fn(); const room = setupRoom([defaultEvent]); diff --git a/test/components/views/location/Map-test.tsx b/test/components/views/location/Map-test.tsx index a1e1680b18a..72826a9cdf2 100644 --- a/test/components/views/location/Map-test.tsx +++ b/test/components/views/location/Map-test.tsx @@ -125,7 +125,7 @@ describe('', () => { const bounds = { north: 51, south: 50, east: 42, west: 41 }; getComponent({ bounds }); expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds([bounds.west, bounds.south], - [bounds.east, bounds.north]), { padding: 100 }); + [bounds.east, bounds.north]), { padding: 100, maxZoom: 15 }); }); it('handles invalid bounds', () => { From bbe0c945d3c0ad6a92ec7ca47b9273d36c32564c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 17:57:08 +0100 Subject: [PATCH 045/102] Fix regression around haveRendererForEvent for hidden events (#8379) --- src/components/structures/MessagePanel.tsx | 8 ---- src/events/EventTileFactory.tsx | 2 + .../structures/MessagePanel-test.js | 39 ++++++++++++++++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index ff9bc826528..8600da90475 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -452,14 +452,6 @@ export default class MessagePanel extends React.Component { } } - /* check the scroll state and send out pagination requests if necessary. - */ - public checkFillState(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.checkFillState(); - } - } - private isUnmounting = (): boolean => { return !this.isMounted; }; diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 7beda8cc305..ecda002f591 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -393,6 +393,8 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo return hasText(mxEvent, showHiddenEvents); } else if (handler === STATE_EVENT_TILE_TYPES[EventType.RoomCreate]) { return Boolean(mxEvent.getContent()['predecessor']); + } else if (handler === JSONEventFactory) { + return false; } else { return true; } diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 3c774290005..eca8c39bec9 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -44,6 +44,8 @@ class WrappedMessagePanel extends React.Component { callEventGroupers = new Map(); render() { + const { showHiddenEvents, ...props } = this.props; + const roomContext = { room, roomId: room.roomId, @@ -54,13 +56,14 @@ class WrappedMessagePanel extends React.Component { showJoinLeaves: false, showAvatarChanges: false, showDisplaynameChanges: true, + showHiddenEvents, }; return @@ -633,6 +636,40 @@ describe('MessagePanel', function() { expect(settingsSpy).not.toHaveBeenCalledWith("showHiddenEventsInTimeline"); settingsSpy.mockRestore(); }); + + it("should group hidden event reactions into an event list summary", () => { + const events = [ + TestUtilsMatrix.mkEvent({ + event: true, + type: "m.reaction", + room: "!room:id", + user: "@user:id", + content: {}, + ts: 1, + }), + TestUtilsMatrix.mkEvent({ + event: true, + type: "m.reaction", + room: "!room:id", + user: "@user:id", + content: {}, + ts: 2, + }), + TestUtilsMatrix.mkEvent({ + event: true, + type: "m.reaction", + room: "!room:id", + user: "@user:id", + content: {}, + ts: 3, + }), + ]; + const res = mount(); + + const els = res.find("EventListSummary"); + expect(els.length).toEqual(1); + expect(els.prop("events").length).toEqual(3); + }); }); describe("shouldFormContinuation", () => { From 495a69532d4ddc2fe3e10fe8260869bdb68f99e9 Mon Sep 17 00:00:00 2001 From: Yaya Usman <38439166+yaya-usman@users.noreply.github.com> Date: Fri, 22 Apr 2022 00:13:39 +0300 Subject: [PATCH 046/102] fix: "Mention highlight and cursor hover highlight has different corner radius" (#8384) * fix: mention tile and hover highlight's same border radius * style-lint fix * update: mention tile and hover highlight's same border radius --- res/css/views/rooms/_EventTile.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index f28fa3b71d4..497de258dc5 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -223,12 +223,6 @@ $left-gutter: 64px; overflow-y: hidden; } - &.mx_EventTile_selected .mx_EventTile_line, - &:hover .mx_EventTile_line { - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - } - &:hover.mx_EventTile_verified .mx_EventTile_line { box-shadow: inset calc(50px + $selected-message-border-width) 0 0 -50px $e2e-verified-color; } From 763edb7ab9302d6908ff84bafe5801bc390f47c6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 22 Apr 2022 08:37:23 +0100 Subject: [PATCH 047/102] Fix issue with thread panel not updating when it loads on first render (#8382) --- src/components/structures/ThreadPanel.tsx | 12 +++++++----- src/components/structures/TimelinePanel.tsx | 7 ++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 3009bedae3e..eccb68ed997 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -198,9 +198,10 @@ const ThreadPanel: React.FC = ({ useEffect(() => { const room = mxClient.getRoom(roomId); room.createThreadsTimelineSets().then(() => { - setRoom(room); + return room.fetchRoomThreads(); + }).then(() => { setFilterOption(ThreadFilterType.All); - room.fetchRoomThreads(); + setRoom(room); }); }, [mxClient, roomId]); @@ -286,8 +287,8 @@ const ThreadPanel: React.FC = ({ sensor={card.current} onMeasurement={setNarrow} /> - { timelineSet && ( - = ({ permalinkCreator={permalinkCreator} disableGrouping={true} /> - ) } + :
    + } ); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index bc4ea5bdcbe..7201e3c6f2e 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1188,11 +1188,8 @@ class TimelinePanel extends React.Component { const onLoaded = () => { if (this.unmounted) return; - // clear the timeline min-height when - // (re)loading the timeline - if (this.messagePanel.current) { - this.messagePanel.current.onTimelineReset(); - } + // clear the timeline min-height when (re)loading the timeline + this.messagePanel.current?.onTimelineReset(); this.reloadEvents(); // If we switched away from the room while there were pending From a3a7c60dd7731bb4a253c918984ca7fa0a9ddb7a Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 22 Apr 2022 13:38:27 +0200 Subject: [PATCH 048/102] LLS: Remove beacon info illegal replace relation (#8390) * dont apply illegal replace relation to beacon_info event Signed-off-by: Kerry Archibald * only display tiles for beacon infos with live prop Signed-off-by: Kerry Archibald * copyrights Signed-off-by: Kerry Archibald --- src/events/EventTileFactory.tsx | 6 +++-- src/stores/OwnBeaconStore.ts | 11 +------- src/utils/beacon/timeline.ts | 27 ++++++++++++++++++++ test/stores/OwnBeaconStore-test.ts | 25 ------------------- test/utils/beacon/timeline-test.ts | 40 ++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 src/utils/beacon/timeline.ts create mode 100644 test/utils/beacon/timeline-test.ts diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index ecda002f591..f122329ee5a 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -19,7 +19,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event"; import { M_POLL_START, Optional } from "matrix-events-sdk"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import EditorStateTransfer from "../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks"; @@ -44,6 +43,7 @@ import { getMessageModerationState, MessageModerationState } from "../utils/Even import HiddenBody from "../components/views/messages/HiddenBody"; import SettingsStore from "../settings/SettingsStore"; import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; +import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps { @@ -219,7 +219,9 @@ export function pickFactory( // Try and pick a state event factory, if we can. if (mxEvent.isState()) { if ( - M_BEACON_INFO.matches(evType) && + shouldDisplayAsBeaconTile(mxEvent) && + // settings store access here temporarily during labs + // only hit when a beacon_info event is hit SettingsStore.getValue("feature_location_share_live") ) { return MessageEventFactory; diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index eabe9ea083b..1bef120ae70 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -20,7 +20,6 @@ import { BeaconIdentifier, BeaconEvent, MatrixEvent, - RelationType, Room, RoomMember, RoomState, @@ -449,20 +448,12 @@ export class OwnBeaconStore extends AsyncStoreWithClient { ...update, }; - const newContent = makeBeaconInfoContent(timeout, + const updateContent = makeBeaconInfoContent(timeout, live, description, assetType, timestamp, ); - const updateContent = { - ...newContent, - "m.new_content": newContent, - "m.relates_to": { - "rel_type": RelationType.Replace, - "event_id": beacon.beaconInfoId, - }, - }; await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, updateContent); }; diff --git a/src/utils/beacon/timeline.ts b/src/utils/beacon/timeline.ts new file mode 100644 index 00000000000..e6e8f2311d5 --- /dev/null +++ b/src/utils/beacon/timeline.ts @@ -0,0 +1,27 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; + +/** + * beacon_info events without live property set to true + * should be displayed in the timeline + */ +export const shouldDisplayAsBeaconTile = (event: MatrixEvent): boolean => ( + M_BEACON_INFO.matches(event.getType()) && + !!event.getContent()?.live +); diff --git a/test/stores/OwnBeaconStore-test.ts b/test/stores/OwnBeaconStore-test.ts index b5713f6348f..6a95c82ad07 100644 --- a/test/stores/OwnBeaconStore-test.ts +++ b/test/stores/OwnBeaconStore-test.ts @@ -20,7 +20,6 @@ import { BeaconEvent, getBeaconInfoIdentifier, MatrixEvent, - RelationType, RoomStateEvent, RoomMember, } from "matrix-js-sdk/src/matrix"; @@ -473,14 +472,6 @@ describe('OwnBeaconStore', () => { const expectedUpdateContent = { ...prevEventContent, live: false, - ["m.new_content"]: { - ...prevEventContent, - live: false, - }, - ["m.relates_to"]: { - event_id: alicesRoom1BeaconInfo.getId(), - rel_type: RelationType.Replace, - }, }; expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( room1Id, @@ -650,14 +641,6 @@ describe('OwnBeaconStore', () => { const expectedUpdateContent = { ...prevEventContent, live: false, - ["m.new_content"]: { - ...prevEventContent, - live: false, - }, - ["m.relates_to"]: { - event_id: alicesRoom1BeaconInfo.getId(), - rel_type: RelationType.Replace, - }, }; expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( room1Id, @@ -683,14 +666,6 @@ describe('OwnBeaconStore', () => { const expectedUpdateContent = { ...prevEventContent, live: false, - ["m.new_content"]: { - ...prevEventContent, - live: false, - }, - ["m.relates_to"]: { - event_id: alicesRoom1BeaconInfo.getId(), - rel_type: RelationType.Replace, - }, }; expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( room1Id, diff --git a/test/utils/beacon/timeline-test.ts b/test/utils/beacon/timeline-test.ts new file mode 100644 index 00000000000..96be565de74 --- /dev/null +++ b/test/utils/beacon/timeline-test.ts @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { shouldDisplayAsBeaconTile } from "../../../src/utils/beacon/timeline"; +import { makeBeaconInfoEvent } from "../../test-utils"; + +describe('shouldDisplayAsBeaconTile', () => { + const userId = '@user:server'; + const roomId = '!room:server'; + const liveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: true }); + const notLiveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: false }); + const memberEvent = new MatrixEvent({ type: EventType.RoomMember }); + + it('returns true for a beacon with live property set to true', () => { + expect(shouldDisplayAsBeaconTile(liveBeacon)).toBe(true); + }); + + it('returns false for a beacon with live property set to false', () => { + expect(shouldDisplayAsBeaconTile(notLiveBeacon)).toBe(false); + }); + + it('returns false for a non beacon event', () => { + expect(shouldDisplayAsBeaconTile(memberEvent)).toBe(false); + }); +}); From 988d3002584e66fe60ae783410efe8fc7026db03 Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 22 Apr 2022 14:05:36 +0200 Subject: [PATCH 049/102] Live location sharing: only share to beacons created on device (#8378) * create live beacons in ownbeaconstore and test Signed-off-by: Kerry Archibald * more mocks in RoomLiveShareWarning Signed-off-by: Kerry Archibald * extend mocks in components Signed-off-by: Kerry Archibald * comment Signed-off-by: Kerry Archibald * remove another comment Signed-off-by: Kerry Archibald * extra ? hedge in roommembers change Signed-off-by: Kerry Archibald * listen to destroy and prune local store on stop Signed-off-by: Kerry Archibald * tests Signed-off-by: Kerry Archibald * update copy pasted copyright to 2022 Signed-off-by: Kerry Archibald --- .../views/location/shareLocation.ts | 3 +- src/stores/OwnBeaconStore.ts | 78 +++++- src/utils/beacon/timeline.ts | 2 +- .../beacon/RoomLiveShareWarning-test.tsx | 11 + .../views/location/LocationShareMenu-test.tsx | 31 ++- test/stores/OwnBeaconStore-test.ts | 234 +++++++++++++++++- test/utils/beacon/timeline-test.ts | 2 +- 7 files changed, 341 insertions(+), 20 deletions(-) diff --git a/src/components/views/location/shareLocation.ts b/src/components/views/location/shareLocation.ts index 6654a389a06..895f2c23c6c 100644 --- a/src/components/views/location/shareLocation.ts +++ b/src/components/views/location/shareLocation.ts @@ -25,6 +25,7 @@ import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import QuestionDialog from "../dialogs/QuestionDialog"; import SdkConfig from "../../../SdkConfig"; +import { OwnBeaconStore } from "../../../stores/OwnBeaconStore"; export enum LocationShareType { Own = 'Own', @@ -70,7 +71,7 @@ export const shareLiveLocation = ( ): ShareLocationFn => async ({ timeout }) => { const description = _t(`%(displayName)s's live location`, { displayName }); try { - await client.unstable_createLiveBeacon( + await OwnBeaconStore.instance.createLiveBeacon( roomId, makeBeaconInfoContent( timeout ?? DEFAULT_LIVE_DURATION, diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index 1bef120ae70..b4b4a4328a7 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -28,7 +28,7 @@ import { import { BeaconInfoState, makeBeaconContent, makeBeaconInfoContent, } from "matrix-js-sdk/src/content-helpers"; -import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; +import { MBeaconInfoEventContent, M_BEACON } from "matrix-js-sdk/src/@types/beacon"; import { logger } from "matrix-js-sdk/src/logger"; import defaultDispatcher from "../dispatcher/dispatcher"; @@ -64,6 +64,30 @@ type OwnBeaconStoreState = { beaconsByRoomId: Map>; liveBeaconIds: BeaconIdentifier[]; }; + +const CREATED_BEACONS_KEY = 'mx_live_beacon_created_id'; +const removeLocallyCreateBeaconEventId = (eventId: string): void => { + const ids = getLocallyCreatedBeaconEventIds(); + window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify(ids.filter(id => id !== eventId))); +}; +const storeLocallyCreateBeaconEventId = (eventId: string): void => { + const ids = getLocallyCreatedBeaconEventIds(); + window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify([...ids, eventId])); +}; + +const getLocallyCreatedBeaconEventIds = (): string[] => { + let ids: string[]; + try { + ids = JSON.parse(window.localStorage.getItem(CREATED_BEACONS_KEY) ?? '[]'); + if (!Array.isArray(ids)) { + throw new Error('Invalid stored value'); + } + } catch (error) { + logger.error('Failed to retrieve locally created beacon event ids', error); + ids = []; + } + return ids; +}; export class OwnBeaconStore extends AsyncStoreWithClient { private static internalInstance = new OwnBeaconStore(); // users beacons, keyed by event type @@ -110,6 +134,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness); this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon); this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon); + this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon); this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers); this.beacons.forEach(beacon => beacon.destroy()); @@ -125,6 +150,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness); this.matrixClient.on(BeaconEvent.New, this.onNewBeacon); this.matrixClient.on(BeaconEvent.Update, this.onUpdateBeacon); + this.matrixClient.on(BeaconEvent.Destroy, this.onDestroyBeacon); this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers); this.initialiseBeaconState(); @@ -188,7 +214,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient { return; } - return await this.updateBeaconEvent(beacon, { live: false }); + await this.updateBeaconEvent(beacon, { live: false }); + + // prune from local store + removeLocallyCreateBeaconEventId(beacon.beaconInfoId); }; /** @@ -215,6 +244,15 @@ export class OwnBeaconStore extends AsyncStoreWithClient { beacon.monitorLiveness(); }; + private onDestroyBeacon = (beaconIdentifier: BeaconIdentifier): void => { + // check if we care about this beacon + if (!this.beacons.has(beaconIdentifier)) { + return; + } + + this.checkLiveness(); + }; + private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => { // check if we care about this beacon if (!this.beacons.has(beacon.identifier)) { @@ -249,7 +287,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { // stop watching beacons in rooms where user is no longer a member if (member.membership === 'leave' || member.membership === 'ban') { - this.beaconsByRoomId.get(roomState.roomId).forEach(this.removeBeacon); + this.beaconsByRoomId.get(roomState.roomId)?.forEach(this.removeBeacon); this.beaconsByRoomId.delete(roomState.roomId); } }; @@ -308,9 +346,14 @@ export class OwnBeaconStore extends AsyncStoreWithClient { }; private checkLiveness = (): void => { + const locallyCreatedBeaconEventIds = getLocallyCreatedBeaconEventIds(); const prevLiveBeaconIds = this.getLiveBeaconIds(); this.liveBeaconIds = [...this.beacons.values()] - .filter(beacon => beacon.isLive) + .filter(beacon => + beacon.isLive && + // only beacons created on this device should be shared to + locallyCreatedBeaconEventIds.includes(beacon.beaconInfoId), + ) .sort(sortBeaconsByLatestCreation) .map(beacon => beacon.identifier); @@ -339,6 +382,32 @@ export class OwnBeaconStore extends AsyncStoreWithClient { } }; + public createLiveBeacon = async ( + roomId: Room['roomId'], + beaconInfoContent: MBeaconInfoEventContent, + ): Promise => { + // eslint-disable-next-line camelcase + const { event_id } = await this.matrixClient.unstable_createLiveBeacon( + roomId, + beaconInfoContent, + ); + + storeLocallyCreateBeaconEventId(event_id); + + // try to stop any other live beacons + // in this room + this.beaconsByRoomId.get(roomId)?.forEach(beaconId => { + if (this.getBeaconById(beaconId)?.isLive) { + try { + // don't await, this is best effort + this.stopBeacon(beaconId); + } catch (error) { + logger.error('Failed to stop live beacons', error); + } + } + }); + }; + /** * Geolocation */ @@ -420,7 +489,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.stopPollingLocation(); // kill live beacons when location permissions are revoked - // TODO may need adjustment when PSF-797 is done await Promise.all(this.liveBeaconIds.map(this.stopBeacon)); }; diff --git a/src/utils/beacon/timeline.ts b/src/utils/beacon/timeline.ts index e6e8f2311d5..d00872f8651 100644 --- a/src/utils/beacon/timeline.ts +++ b/src/utils/beacon/timeline.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +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. diff --git a/test/components/views/beacon/RoomLiveShareWarning-test.tsx b/test/components/views/beacon/RoomLiveShareWarning-test.tsx index 6fa6098f2c7..030ac8ea4af 100644 --- a/test/components/views/beacon/RoomLiveShareWarning-test.tsx +++ b/test/components/views/beacon/RoomLiveShareWarning-test.tsx @@ -93,10 +93,20 @@ describe('', () => { return component; }; + const localStorageSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined); + beforeEach(() => { mockGeolocation(); jest.spyOn(global.Date, 'now').mockReturnValue(now); mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' }); + + // assume all beacons were created on this device + localStorageSpy.mockReturnValue(JSON.stringify([ + room1Beacon1.getId(), + room2Beacon1.getId(), + room2Beacon2.getId(), + room3Beacon1.getId(), + ])); }); afterEach(async () => { @@ -106,6 +116,7 @@ describe('', () => { afterAll(() => { jest.spyOn(global.Date, 'now').mockRestore(); + localStorageSpy.mockRestore(); }); const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text(); diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index e873b9db89d..ce52125f86f 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -29,9 +29,15 @@ import { ChevronFace } from '../../../../src/components/structures/ContextMenu'; import SettingsStore from '../../../../src/settings/SettingsStore'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; import { LocationShareType } from '../../../../src/components/views/location/shareLocation'; -import { findByTagAndTestId, flushPromises } from '../../../test-utils'; +import { + findByTagAndTestId, + flushPromises, + getMockClientWithEventEmitter, + setupAsyncStoreWithClient, +} from '../../../test-utils'; import Modal from '../../../../src/Modal'; import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown'; +import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore'; jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({ findMapStyleUrl: jest.fn().mockReturnValue('test'), @@ -57,17 +63,15 @@ jest.mock('../../../../src/Modal', () => ({ describe('', () => { const userId = '@ernie:server.org'; - const mockClient = { - on: jest.fn(), - off: jest.fn(), - removeListener: jest.fn(), + const mockClient = getMockClientWithEventEmitter({ getUserId: jest.fn().mockReturnValue(userId), getClientWellKnown: jest.fn().mockResolvedValue({ map_style_url: 'maps.com', }), sendMessage: jest.fn(), - unstable_createLiveBeacon: jest.fn().mockResolvedValue({}), - }; + unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), + getVisibleRooms: jest.fn().mockReturnValue([]), + }); const defaultProps = { menuPosition: { @@ -90,19 +94,28 @@ describe('', () => { type: 'geolocate', }; + const makeOwnBeaconStore = async () => { + const store = OwnBeaconStore.instance; + + await setupAsyncStoreWithClient(store, mockClient); + return store; + }; + const getComponent = (props = {}) => mount(, { wrappingComponent: MatrixClientContext.Provider, wrappingComponentProps: { value: mockClient }, }); - beforeEach(() => { + beforeEach(async () => { jest.spyOn(logger, 'error').mockRestore(); mocked(SettingsStore).getValue.mockReturnValue(false); mockClient.sendMessage.mockClear(); - mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue(undefined); + mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' }); jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient); mocked(Modal).createTrackedDialog.mockClear(); + + await makeOwnBeaconStore(); }); const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) => diff --git a/test/stores/OwnBeaconStore-test.ts b/test/stores/OwnBeaconStore-test.ts index 6a95c82ad07..df1d868e40f 100644 --- a/test/stores/OwnBeaconStore-test.ts +++ b/test/stores/OwnBeaconStore-test.ts @@ -23,7 +23,7 @@ import { RoomStateEvent, RoomMember, } from "matrix-js-sdk/src/matrix"; -import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers"; +import { makeBeaconContent, makeBeaconInfoContent } from "matrix-js-sdk/src/content-helpers"; import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; import { logger } from "matrix-js-sdk/src/logger"; @@ -64,6 +64,7 @@ describe('OwnBeaconStore', () => { getVisibleRooms: jest.fn().mockReturnValue([]), unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), sendEvent: jest.fn().mockResolvedValue({ event_id: '1' }), + unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), }); const room1Id = '$room1:server.org'; const room2Id = '$room2:server.org'; @@ -144,6 +145,7 @@ describe('OwnBeaconStore', () => { beaconInfoEvent.getSender(), beaconInfoEvent.getRoomId(), { isLive, timeout: beacon.beaconInfo.timeout }, + 'update-event-id', ); beacon.update(updateEvent); @@ -156,6 +158,9 @@ describe('OwnBeaconStore', () => { mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon); }; + const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined); + const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem').mockImplementation(() => {}); + beforeEach(() => { geolocation = mockGeolocation(); mockClient.getVisibleRooms.mockReturnValue([]); @@ -164,6 +169,9 @@ describe('OwnBeaconStore', () => { jest.spyOn(global.Date, 'now').mockReturnValue(now); jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore(); jest.spyOn(logger, 'error').mockRestore(); + + localStorageGetSpy.mockClear().mockReturnValue(undefined); + localStorageSetSpy.mockClear(); }); afterEach(async () => { @@ -172,6 +180,10 @@ describe('OwnBeaconStore', () => { jest.clearAllTimers(); }); + afterAll(() => { + localStorageGetSpy.mockRestore(); + }); + describe('onReady()', () => { it('initialises correctly with no beacons', async () => { makeRoomsWithStateEvents(); @@ -195,7 +207,27 @@ describe('OwnBeaconStore', () => { bobsOldRoom1BeaconInfo, ]); const store = await makeOwnBeaconStore(); - expect(store.hasLiveBeacons()).toBe(true); + expect(store.beaconsByRoomId.get(room1Id)).toEqual(new Set([ + getBeaconInfoIdentifier(alicesRoom1BeaconInfo), + ])); + expect(store.beaconsByRoomId.get(room2Id)).toEqual(new Set([ + getBeaconInfoIdentifier(alicesRoom2BeaconInfo), + ])); + }); + + it('updates live beacon ids when users own beacons were created on device', async () => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + ])); + makeRoomsWithStateEvents([ + alicesRoom1BeaconInfo, + alicesRoom2BeaconInfo, + bobsRoom1BeaconInfo, + bobsOldRoom1BeaconInfo, + ]); + const store = await makeOwnBeaconStore(); + expect(store.hasLiveBeacons(room1Id)).toBeTruthy(); expect(store.getLiveBeaconIds()).toEqual([ getBeaconInfoIdentifier(alicesRoom1BeaconInfo), getBeaconInfoIdentifier(alicesRoom2BeaconInfo), @@ -214,6 +246,10 @@ describe('OwnBeaconStore', () => { }); it('does geolocation and sends location immediatley when user has live beacons', async () => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + ])); makeRoomsWithStateEvents([ alicesRoom1BeaconInfo, alicesRoom2BeaconInfo, @@ -245,7 +281,8 @@ describe('OwnBeaconStore', () => { expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange])); expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New])); expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([BeaconEvent.Update])); - expect(removeSpy.mock.calls[3]).toEqual(expect.arrayContaining([RoomStateEvent.Members])); + expect(removeSpy.mock.calls[3]).toEqual(expect.arrayContaining([BeaconEvent.Destroy])); + expect(removeSpy.mock.calls[4]).toEqual(expect.arrayContaining([RoomStateEvent.Members])); }); it('destroys beacons', async () => { @@ -270,6 +307,10 @@ describe('OwnBeaconStore', () => { bobsRoom1BeaconInfo, bobsOldRoom1BeaconInfo, ]); + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + ])); }); it('returns true when user has live beacons', async () => { @@ -320,6 +361,10 @@ describe('OwnBeaconStore', () => { bobsRoom1BeaconInfo, bobsOldRoom1BeaconInfo, ]); + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + ])); }); it('returns live beacons when user has live beacons', async () => { @@ -371,6 +416,13 @@ describe('OwnBeaconStore', () => { }); describe('on new beacon event', () => { + // assume all beacons were created on this device + beforeEach(() => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + ])); + }); it('ignores events for irrelevant beacons', async () => { makeRoomsWithStateEvents([]); const store = await makeOwnBeaconStore(); @@ -425,6 +477,16 @@ describe('OwnBeaconStore', () => { }); describe('on liveness change event', () => { + // assume all beacons were created on this device + beforeEach(() => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + alicesOldRoomIdBeaconInfo.getId(), + 'update-event-id', + ])); + }); + it('ignores events for irrelevant beacons', async () => { makeRoomsWithStateEvents([ alicesRoom1BeaconInfo, @@ -501,6 +563,13 @@ describe('OwnBeaconStore', () => { }); describe('on room membership changes', () => { + // assume all beacons were created on this device + beforeEach(() => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + ])); + }); it('ignores events for rooms without beacons', async () => { const membershipEvent = makeMembershipEvent(room2Id, aliceId); // no beacons for room2 @@ -606,6 +675,54 @@ describe('OwnBeaconStore', () => { }); }); + describe('on destroy event', () => { + // assume all beacons were created on this device + beforeEach(() => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + alicesOldRoomIdBeaconInfo.getId(), + 'update-event-id', + ])); + }); + + it('ignores events for irrelevant beacons', async () => { + makeRoomsWithStateEvents([ + alicesRoom1BeaconInfo, + ]); + const store = await makeOwnBeaconStore(); + const emitSpy = jest.spyOn(store, 'emit'); + const oldLiveBeaconIds = store.getLiveBeaconIds(); + const bobsLiveBeacon = new Beacon(bobsRoom1BeaconInfo); + + mockClient.emit(BeaconEvent.Destroy, bobsLiveBeacon.identifier); + + expect(emitSpy).not.toHaveBeenCalled(); + // strictly equal + expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds); + }); + + it('updates state and emits beacon liveness changes from true to false', async () => { + makeRoomsWithStateEvents([ + alicesRoom1BeaconInfo, + ]); + const store = await makeOwnBeaconStore(); + + // live before + expect(store.hasLiveBeacons()).toBe(true); + const emitSpy = jest.spyOn(store, 'emit'); + + const beacon = store.getBeaconById(getBeaconInfoIdentifier(alicesRoom1BeaconInfo)); + + beacon.destroy(); + mockClient.emit(BeaconEvent.Destroy, beacon.identifier); + + expect(store.hasLiveBeacons()).toBe(false); + expect(store.hasLiveBeacons(room1Id)).toBe(false); + expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, []); + }); + }); + describe('stopBeacon()', () => { beforeEach(() => { makeRoomsWithStateEvents([ @@ -672,9 +789,38 @@ describe('OwnBeaconStore', () => { expectedUpdateContent, ); }); + + it('removes beacon event id from local store', async () => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + ])); + makeRoomsWithStateEvents([ + alicesRoom1BeaconInfo, + ]); + const store = await makeOwnBeaconStore(); + + await store.stopBeacon(getBeaconInfoIdentifier(alicesRoom1BeaconInfo)); + + expect(localStorageSetSpy).toHaveBeenCalledWith( + 'mx_live_beacon_created_id', + // stopped beacon's event_id was removed + JSON.stringify([alicesRoom2BeaconInfo.getId()]), + ); + }); }); describe('publishing positions', () => { + // assume all beacons were created on this device + beforeEach(() => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + alicesRoom2BeaconInfo.getId(), + alicesOldRoomIdBeaconInfo.getId(), + 'update-event-id', + ])); + }); + it('stops watching position when user has no more live beacons', async () => { // geolocation is only going to emit 1 position geolocation.watchPosition.mockImplementation( @@ -842,6 +988,7 @@ describe('OwnBeaconStore', () => { // called for each position from watchPosition expect(mockClient.sendEvent).toHaveBeenCalledTimes(5); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false); + expect(store.getLiveBeaconIdsWithWireError()).toEqual([]); expect(store.hasWireErrors()).toBe(false); }); @@ -892,6 +1039,12 @@ describe('OwnBeaconStore', () => { // only two allowed failures expect(mockClient.sendEvent).toHaveBeenCalledTimes(2); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(true); + expect(store.getLiveBeaconIdsWithWireError()).toEqual( + [getBeaconInfoIdentifier(alicesRoom1BeaconInfo)], + ); + expect(store.getLiveBeaconIdsWithWireError(room1Id)).toEqual( + [getBeaconInfoIdentifier(alicesRoom1BeaconInfo)], + ); expect(store.hasWireErrors()).toBe(true); expect(emitSpy).toHaveBeenCalledWith( OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo), @@ -1055,4 +1208,79 @@ describe('OwnBeaconStore', () => { expect(mockClient.sendEvent).not.toHaveBeenCalled(); }); }); + + describe('createLiveBeacon', () => { + const newEventId = 'new-beacon-event-id'; + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation(() => {}); + beforeEach(() => { + localStorageGetSpy.mockReturnValue(JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + ])); + + localStorageSetSpy.mockClear(); + + mockClient.unstable_createLiveBeacon.mockResolvedValue({ event_id: newEventId }); + }); + + it('creates a live beacon', async () => { + const store = await makeOwnBeaconStore(); + const content = makeBeaconInfoContent(100); + await store.createLiveBeacon(room1Id, content); + expect(mockClient.unstable_createLiveBeacon).toHaveBeenCalledWith(room1Id, content); + }); + + it('sets new beacon event id in local storage', async () => { + const store = await makeOwnBeaconStore(); + const content = makeBeaconInfoContent(100); + await store.createLiveBeacon(room1Id, content); + + expect(localStorageSetSpy).toHaveBeenCalledWith( + 'mx_live_beacon_created_id', + JSON.stringify([ + alicesRoom1BeaconInfo.getId(), + newEventId, + ]), + ); + }); + + it('handles saving beacon event id when local storage has bad value', async () => { + localStorageGetSpy.mockReturnValue(JSON.stringify({ id: '1' })); + const store = await makeOwnBeaconStore(); + const content = makeBeaconInfoContent(100); + await store.createLiveBeacon(room1Id, content); + + // stored successfully + expect(localStorageSetSpy).toHaveBeenCalledWith( + 'mx_live_beacon_created_id', + JSON.stringify([ + newEventId, + ]), + ); + }); + + it('creates a live beacon without error when no beacons exist for room', async () => { + const store = await makeOwnBeaconStore(); + const content = makeBeaconInfoContent(100); + await store.createLiveBeacon(room1Id, content); + + // didn't throw, no error log + expect(loggerErrorSpy).not.toHaveBeenCalled(); + }); + + it('stops live beacons for room after creating new beacon', async () => { + // room1 already has a beacon + makeRoomsWithStateEvents([ + alicesRoom1BeaconInfo, + ]); + // but it was not created on this device + localStorageGetSpy.mockReturnValue(undefined); + + const store = await makeOwnBeaconStore(); + const content = makeBeaconInfoContent(100); + await store.createLiveBeacon(room1Id, content); + + // update beacon called + expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled(); + }); + }); }); diff --git a/test/utils/beacon/timeline-test.ts b/test/utils/beacon/timeline-test.ts index 96be565de74..59217d24590 100644 --- a/test/utils/beacon/timeline-test.ts +++ b/test/utils/beacon/timeline-test.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +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. From 2a396a406d7e13a534bf34350e4d590c68222816 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 Apr 2022 08:17:31 -0400 Subject: [PATCH 050/102] Stick connected video rooms to the top of the room list (#8353) --- src/stores/room-list/RoomListStore.ts | 5 +- src/stores/room-list/algorithms/Algorithm.ts | 89 +++++++++++++++++-- .../room-list/algorithms/Algorithm-test.ts | 64 +++++++++++++ 3 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 test/stores/room-list/algorithms/Algorithm-test.ts diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 125b045c9a4..dc7f405c62d 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -70,6 +70,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { constructor() { super(defaultDispatcher); this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares + this.algorithm.start(); } private setupWatchers() { @@ -96,6 +97,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated); + this.algorithm.stop(); this.algorithm = new Algorithm(); this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); this.algorithm.on(FILTER_CHANGED, this.onAlgorithmListUpdated); @@ -479,8 +481,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } } - private onAlgorithmListUpdated = () => { + private onAlgorithmListUpdated = (forceUpdate: boolean) => { this.updateFn.mark(); + if (forceUpdate) this.updateFn.trigger(); }; private onAlgorithmFilterUpdated = () => { diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 55e877ed5d4..edd406fd07b 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -35,6 +35,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; import { VisibilityProvider } from "../filters/VisibilityProvider"; +import VideoChannelStore, { VideoChannelEvent } from "../../VideoChannelStore"; /** * Fired when the Algorithm has determined a list has been updated. @@ -84,8 +85,14 @@ export class Algorithm extends EventEmitter { */ public updatesInhibited = false; - public constructor() { - super(); + public start() { + VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoRoom); + VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoRoom); + } + + public stop() { + VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoRoom); + VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoRoom); } public get stickyRoom(): Room { @@ -108,6 +115,7 @@ export class Algorithm extends EventEmitter { this._cachedRooms = val; this.recalculateFilteredRooms(); this.recalculateStickyRoom(); + this.recalculateVideoRoom(); } protected get cachedRooms(): ITagMap { @@ -145,6 +153,7 @@ export class Algorithm extends EventEmitter { this._cachedRooms[tagId] = algorithm.orderedRooms; this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed + this.recalculateVideoRoom(tagId); } public getListOrdering(tagId: TagID): ListAlgorithm { @@ -164,6 +173,7 @@ export class Algorithm extends EventEmitter { this._cachedRooms[tagId] = algorithm.orderedRooms; this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed + this.recalculateVideoRoom(tagId); } public addFilterCondition(filterCondition: IFilterCondition): void { @@ -311,12 +321,30 @@ export class Algorithm extends EventEmitter { this.recalculateFilteredRoomsForTag(tag); if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag); this.recalculateStickyRoom(); + this.recalculateVideoRoom(tag); + if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateVideoRoom(lastStickyRoom.tag); // Finally, trigger an update if (this.updatesInhibited) return; this.emit(LIST_UPDATED_EVENT); } + /** + * Update the stickiness of video rooms. + */ + public updateVideoRoom = () => { + // In case we're unsticking a video room, sort it back into natural order + this.recalculateFilteredRooms(); + this.recalculateStickyRoom(); + + this.recalculateVideoRoom(); + + if (this.updatesInhibited) return; + // This isn't in response to any particular RoomListStore update, + // so notify the store that it needs to force-update + this.emit(LIST_UPDATED_EVENT, true); + }; + protected recalculateFilteredRooms() { if (!this.hasFilters) { return; @@ -374,6 +402,13 @@ export class Algorithm extends EventEmitter { } } + private initCachedStickyRooms() { + this._cachedStickyRooms = {}; + for (const tagId of Object.keys(this.cachedRooms)) { + this._cachedStickyRooms[tagId] = [...this.cachedRooms[tagId]]; // shallow clone + } + } + /** * Recalculate the sticky room position. If this is being called in relation to * a specific tag being updated, it should be given to this function to optimize @@ -400,17 +435,13 @@ export class Algorithm extends EventEmitter { } if (!this._cachedStickyRooms || !updatedTag) { - const stickiedTagMap: ITagMap = {}; - for (const tagId of Object.keys(this.cachedRooms)) { - stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone - } - this._cachedStickyRooms = stickiedTagMap; + this.initCachedStickyRooms(); } if (updatedTag) { // Update the tag indicated by the caller, if possible. This is mostly to ensure // our cache is up to date. - this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone + this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone } // Now try to insert the sticky room, if we need to. @@ -426,6 +457,46 @@ export class Algorithm extends EventEmitter { this.emit(LIST_UPDATED_EVENT); } + /** + * Recalculate the position of any video rooms. If this is being called in relation to + * a specific tag being updated, it should be given to this function to optimize + * the call. + * + * This expects to be called *after* the sticky rooms are updated, and sticks the + * currently connected video room to the top of its tag. + * + * @param updatedTag The tag that was updated, if possible. + */ + protected recalculateVideoRoom(updatedTag: TagID = null): void { + if (!updatedTag) { + // Assume all tags need updating + // We're not modifying the map here, so can safely rely on the cached values + // rather than the explicitly sticky map. + for (const tagId of Object.keys(this.cachedRooms)) { + if (!tagId) { + throw new Error("Unexpected recursion: falsy tag"); + } + this.recalculateVideoRoom(tagId); + } + return; + } + + const videoRoomId = VideoChannelStore.instance.connected ? VideoChannelStore.instance.roomId : null; + + if (videoRoomId) { + // We operate directly on the sticky rooms map + if (!this._cachedStickyRooms) this.initCachedStickyRooms(); + const rooms = this._cachedStickyRooms[updatedTag]; + const videoRoomIdxInTag = rooms.findIndex(r => r.roomId === videoRoomId); + if (videoRoomIdxInTag < 0) return; // no-op + + const videoRoom = rooms[videoRoomIdxInTag]; + rooms.splice(videoRoomIdxInTag, 1); + rooms.unshift(videoRoom); + this._cachedStickyRooms[updatedTag] = rooms; // re-set because references aren't always safe + } + } + /** * Asks the Algorithm to regenerate all lists, using the tags given * as reference for which lists to generate and which way to generate @@ -706,6 +777,7 @@ export class Algorithm extends EventEmitter { this._cachedRooms[rmTag] = algorithm.orderedRooms; this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed + this.recalculateVideoRoom(rmTag); } for (const addTag of diff.added) { const algorithm: OrderingAlgorithm = this.algorithms[addTag]; @@ -782,6 +854,7 @@ export class Algorithm extends EventEmitter { // Flag that we've done something this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed + this.recalculateVideoRoom(tag); changed = true; } diff --git a/test/stores/room-list/algorithms/Algorithm-test.ts b/test/stores/room-list/algorithms/Algorithm-test.ts new file mode 100644 index 00000000000..d83f6ccca6c --- /dev/null +++ b/test/stores/room-list/algorithms/Algorithm-test.ts @@ -0,0 +1,64 @@ +/* +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 { stubClient, stubVideoChannelStore, mkRoom } from "../../../test-utils"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import { DefaultTagID } from "../../../../src/stores/room-list/models"; +import { SortAlgorithm, ListAlgorithm } from "../../../../src/stores/room-list/algorithms/models"; +import "../../../../src/stores/room-list/RoomListStore"; // must be imported before Algorithm to avoid cycles +import { Algorithm } from "../../../../src/stores/room-list/algorithms/Algorithm"; + +describe("Algorithm", () => { + let videoChannelStore; + let algorithm; + let textRoom; + let videoRoom; + beforeEach(() => { + stubClient(); + const cli = MatrixClientPeg.get(); + DMRoomMap.makeShared(); + videoChannelStore = stubVideoChannelStore(); + algorithm = new Algorithm(); + algorithm.start(); + + textRoom = mkRoom(cli, "!text:example.org"); + videoRoom = mkRoom(cli, "!video:example.org"); + videoRoom.isElementVideoRoom.mockReturnValue(true); + algorithm.populateTags( + { [DefaultTagID.Untagged]: SortAlgorithm.Alphabetic }, + { [DefaultTagID.Untagged]: ListAlgorithm.Natural }, + ); + algorithm.setKnownRooms([textRoom, videoRoom]); + }); + + afterEach(() => { + algorithm.stop(); + }); + + it("sticks video rooms to the top when they connect", () => { + expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]); + videoChannelStore.connect("!video:example.org"); + expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]); + }); + + it("unsticks video rooms from the top when they disconnect", () => { + videoChannelStore.connect("!video:example.org"); + expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]); + videoChannelStore.disconnect(); + expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]); + }); +}); From 03c46770f445f1f264cb3708f77b889e39237d66 Mon Sep 17 00:00:00 2001 From: Esha Goel <54431564+goelesha@users.noreply.github.com> Date: Fri, 22 Apr 2022 20:14:07 +0530 Subject: [PATCH 051/102] Changed font-weight to 400 to support light weight font (#8345) * changed font-weight to 400 to support light weight font * removed font-weight line and _CreateRoom.scss file * removed _CreateRoom from _components.scss --- res/css/_common.scss | 1 - res/css/_components.scss | 1 - res/css/structures/_CreateRoom.scss | 36 ----------------------------- 3 files changed, 38 deletions(-) delete mode 100644 res/css/structures/_CreateRoom.scss diff --git a/res/css/_common.scss b/res/css/_common.scss index b5d873194dd..6854b485ed9 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -294,7 +294,6 @@ legend { background-color: $background; color: $light-fg-color; z-index: 4012; - font-weight: 300; font-size: $font-15px; position: relative; padding: 24px; diff --git a/res/css/_components.scss b/res/css/_components.scss index f4b833bdd47..bcd3111663e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -27,7 +27,6 @@ @import "./structures/_BackdropPanel.scss"; @import "./structures/_CompatibilityPage.scss"; @import "./structures/_ContextualMenu.scss"; -@import "./structures/_CreateRoom.scss"; @import "./structures/_FileDropTarget.scss"; @import "./structures/_FilePanel.scss"; @import "./structures/_GenericErrorPage.scss"; diff --git a/res/css/structures/_CreateRoom.scss b/res/css/structures/_CreateRoom.scss deleted file mode 100644 index 78e6881b108..00000000000 --- a/res/css/structures/_CreateRoom.scss +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_CreateRoom { - width: 960px; - margin-left: auto; - margin-right: auto; - color: $primary-content; -} - -.mx_CreateRoom input, -.mx_CreateRoom textarea { - border-radius: 3px; - border: 1px solid $strong-input-border-color; - font-weight: 300; - font-size: $font-13px; - padding: 9px; - margin-top: 6px; -} - -.mx_CreateRoom_description { - width: 330px; -} From ee2ee3c08c86746346de944f97303c1542e4e7d6 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 22 Apr 2022 17:09:44 +0200 Subject: [PATCH 052/102] Implement new Read Receipt design (#8389) * feat: introduce new alignment types for tooltip * feat: introduce new hook for tooltips * feat: allow using onFocus callback for RovingAccessibleButton * feat: allow using custom class for ContextMenu * feat: allow setting tab index for avatar * refactor: move read receipts out of event tile * feat: implement new read receipt design * feat: update SentReceipt to match new read receipts as well --- res/css/_components.scss | 1 + res/css/views/right_panel/_TimelineCard.scss | 4 +- res/css/views/rooms/_EventBubbleTile.scss | 6 +- res/css/views/rooms/_EventTile.scss | 44 +-- res/css/views/rooms/_GroupLayout.scss | 2 +- res/css/views/rooms/_IRCLayout.scss | 4 +- res/css/views/rooms/_ReadReceiptGroup.scss | 146 ++++++++++ .../roving/RovingAccessibleButton.tsx | 16 +- .../roving/RovingAccessibleTooltipButton.tsx | 16 +- src/components/structures/ContextMenu.tsx | 3 +- src/components/views/avatars/BaseAvatar.tsx | 1 + src/components/views/avatars/MemberAvatar.tsx | 3 +- src/components/views/elements/Tooltip.tsx | 12 + src/components/views/rooms/EventTile.tsx | 227 ++++------------ .../views/rooms/ReadReceiptGroup.tsx | 254 ++++++++++++++++++ .../views/rooms/ReadReceiptMarker.tsx | 52 +--- src/i18n/strings/en_EN.json | 4 +- src/utils/useTooltip.tsx | 24 ++ 18 files changed, 551 insertions(+), 268 deletions(-) create mode 100644 res/css/views/rooms/_ReadReceiptGroup.scss create mode 100644 src/components/views/rooms/ReadReceiptGroup.tsx create mode 100644 src/utils/useTooltip.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index bcd3111663e..b222e8ce232 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -248,6 +248,7 @@ @import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PresenceLabel.scss"; +@import "./views/rooms/_ReadReceiptGroup.scss"; @import "./views/rooms/_RecentlyViewedButton.scss"; @import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_ReplyTile.scss"; diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index 447933a39f9..1b35d16cdd0 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -125,8 +125,8 @@ limitations under the License. padding-left: 36px; } - .mx_EventTile_readAvatars { - top: -10px; + .mx_ReadReceiptGroup { + top: -6px; } .mx_WhoIsTypingTile { diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index ca58c666ccf..4329d37c0eb 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -447,7 +447,7 @@ limitations under the License. } } - .mx_EventTile_readAvatars { + .mx_ReadReceiptGroup { position: absolute; right: -78px; // as close to right gutter without clipping as possible bottom: 0; @@ -585,8 +585,8 @@ limitations under the License. right: 127px; // align with that of right-column bubbles } - .mx_EventTile_readAvatars { - right: -18px; // match alignment to RRs of chat bubbles + .mx_ReadReceiptGroup { + right: -14px; // match alignment to RRs of chat bubbles } &::before { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 497de258dc5..480ac086547 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -22,20 +22,18 @@ $left-gutter: 64px; .mx_EventTile_receiptSent, .mx_EventTile_receiptSending { - // Give it some dimensions so the tooltip can position properly + position: relative; display: inline-block; - width: 14px; - height: 14px; - // We don't use `position: relative` on the element because then it won't line - // up with the other read receipts + width: 16px; + height: 16px; &::before { background-color: $tertiary-content; mask-repeat: no-repeat; mask-position: center; - mask-size: 14px; - width: 14px; - height: 14px; + mask-size: 16px; + width: 16px; + height: 16px; content: ''; position: absolute; top: 0; @@ -349,36 +347,6 @@ $left-gutter: 64px; } } -.mx_EventTile_readAvatars { - position: relative; - display: inline-block; - width: 14px; - height: 14px; - // This aligns the avatar with the last line of the - // message. We want to move it one line up - 2.2rem - top: -2.2rem; - user-select: none; - z-index: 1; -} - -.mx_EventTile_readAvatars .mx_BaseAvatar { - position: absolute; - display: inline-block; - height: $font-14px; - width: $font-14px; - - will-change: left, top; - transition: - left var(--transition-short) ease-out, - top var(--transition-standard) ease-out; -} - -.mx_EventTile_readAvatarRemainder { - color: $event-timestamp-color; - font-size: $font-11px; - position: absolute; -} - .mx_EventTile_bigEmoji { font-size: 48px; line-height: 57px; diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index c054256b6a6..b1d6e8b535b 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -97,7 +97,7 @@ $left-gutter: 64px; top: 3px; } - .mx_EventTile_readAvatars { + .mx_ReadReceiptGroup { // This aligns the avatar with the last line of the // message. We want to move it one line up - 2rem top: -2rem; diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index b0401a447ca..36d045610a3 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -49,8 +49,8 @@ $irc-line-height: $font-18px; order: 5; flex-shrink: 0; - .mx_EventTile_readAvatars { - top: 0.2rem; // ($irc-line-height - avatar height) / 2 + .mx_ReadReceiptGroup { + top: -0.3rem; // ($irc-line-height - avatar height) / 2 } } diff --git a/res/css/views/rooms/_ReadReceiptGroup.scss b/res/css/views/rooms/_ReadReceiptGroup.scss new file mode 100644 index 00000000000..fe40b1263f3 --- /dev/null +++ b/res/css/views/rooms/_ReadReceiptGroup.scss @@ -0,0 +1,146 @@ +/* +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. +*/ + +.mx_ReadReceiptGroup { + position: relative; + display: inline-block; + // This aligns the avatar with the last line of the + // message. We want to move it one line up + // See .mx_GroupLayout .mx_EventTile .mx_EventTile_line in _GroupLayout.scss + top: calc(-$font-22px - 3px); + user-select: none; + z-index: 1; + + .mx_ReadReceiptGroup_button { + display: inline-flex; + flex-direction: row; + height: 16px; + padding: 4px; + border-radius: 6px; + + &.mx_AccessibleButton { + &:hover { + background: $event-selected-color; + } + } + } + + .mx_ReadReceiptGroup_remainder { + color: $secondary-content; + font-size: $font-11px; + line-height: $font-16px; + margin-right: 4px; + } + + .mx_ReadReceiptGroup_container { + position: relative; + display: block; + height: 100%; + + .mx_BaseAvatar { + position: absolute; + display: inline-block; + height: 14px; + width: 14px; + border: 1px solid $background; + border-radius: 100%; + + will-change: left, top; + transition: + left var(--transition-short) ease-out, + top var(--transition-standard) ease-out; + } + } +} + +.mx_ReadReceiptGroup_popup { + max-height: 300px; + width: 220px; + border-radius: 8px; + display: flex; + flex-direction: column; + text-align: left; + font-size: 12px; + line-height: 15px; + + right: 0; + + &.mx_ContextualMenu_top { + top: 8px; + } + + &.mx_ContextualMenu_bottom { + bottom: 8px; + } + + .mx_ReadReceiptGroup_title { + font-size: 12px; + line-height: 15px; + margin: 16px 16px 8px; + font-weight: 600; + // shouldn’t be actually focusable + outline: none; + } + + .mx_AutoHideScrollbar { + .mx_ReadReceiptGroup_person { + display: flex; + flex-direction: row; + padding: 4px; + margin: 0 12px; + border-radius: 8px; + + &:hover { + background: $menu-selected-color; + } + + &:last-child { + margin-bottom: 8px; + } + + .mx_BaseAvatar { + margin: 6px 8px; + align-self: center; + justify-self: center; + } + + .mx_ReadReceiptGroup_name { + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + + p { + margin: 2px 0; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_ReadReceiptGroup_secondary { + color: $secondary-content; + } + } + } + } +} + +.mx_ReadReceiptGroup_person--tooltip { + overflow-y: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index f9ce87db6a7..0d9025dd59f 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -20,13 +20,21 @@ import AccessibleButton from "../../components/views/elements/AccessibleButton"; import { useRovingTabIndex } from "../RovingTabIndex"; import { Ref } from "./types"; -interface IProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { +interface IProps extends Omit, "inputRef" | "tabIndex"> { inputRef?: Ref; } // Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. -export const RovingAccessibleButton: React.FC = ({ inputRef, ...props }) => { - const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); - return ; +export const RovingAccessibleButton: React.FC = ({ inputRef, onFocus, ...props }) => { + const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); + return { + onFocusInternal(); + onFocus?.(event); + }} + inputRef={ref} + tabIndex={isActive ? 0 : -1} + />; }; diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index d9e393d7282..432e8880173 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -21,13 +21,21 @@ import { useRovingTabIndex } from "../RovingTabIndex"; import { Ref } from "./types"; type ATBProps = React.ComponentProps; -interface IProps extends Omit { +interface IProps extends Omit { inputRef?: Ref; } // Wrapper to allow use of useRovingTabIndex for simple AccessibleTooltipButtons outside of React Functional Components. -export const RovingAccessibleTooltipButton: React.FC = ({ inputRef, ...props }) => { - const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); - return ; +export const RovingAccessibleTooltipButton: React.FC = ({ inputRef, onFocus, ...props }) => { + const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); + return { + onFocusInternal(); + onFocus?.(event); + }} + inputRef={ref} + tabIndex={isActive ? 0 : -1} + />; }; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index a3c214eb456..187e55cc392 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -82,6 +82,7 @@ export interface IProps extends IPosition { // whether this context menu should be focus managed. If false it must handle itself managed?: boolean; wrapperClassName?: string; + menuClassName?: string; // If true, this context menu will be mounted as a child to the parent container. Otherwise // it will be mounted to a container at the root of the DOM. @@ -319,7 +320,7 @@ export default class ContextMenu extends React.PureComponent { 'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom, 'mx_ContextualMenu_rightAligned': this.props.rightAligned === true, 'mx_ContextualMenu_bottomAligned': this.props.bottomAligned === true, - }); + }, this.props.menuClassName); const menuStyle: CSSProperties = {}; if (props.menuWidth) { diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 22d2d1ab00d..76eea6cec06 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -45,6 +45,7 @@ interface IProps { onClick?: React.MouseEventHandler; inputRef?: React.RefObject; className?: string; + tabIndex?: number; } const calculateUrls = (url, urls, lowBandwidth) => { diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index c5dd1bfd3c4..09400b7e21d 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -43,6 +43,7 @@ interface IProps extends Omit, "name" | title?: string; style?: any; forceHistorical?: boolean; // true to deny `feature_use_only_current_profiles` usage. Default false. + hideTitle?: boolean; } interface IState { @@ -124,7 +125,7 @@ export default class MemberAvatar extends React.PureComponent { { style.top = baseTop + parentBox.height - 50; style.left = horizontalCenter; style.transform = "translate(-50%)"; + break; + case Alignment.TopRight: + style.top = baseTop - 5; + style.right = width - parentBox.right - window.pageXOffset; + style.transform = "translate(5px, -100%)"; + break; + case Alignment.TopCenter: + style.top = baseTop - 5; + style.left = horizontalCenter; + style.transform = "translate(-50%, -100%)"; } return style; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 4c68ee532e8..f068e029a85 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -36,12 +36,11 @@ import { formatTime } from "../../../DateUtils"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { E2EState } from "./E2EIcon"; -import { toRem } from "../../../utils/units"; import RoomAvatar from "../avatars/RoomAvatar"; import MessageContextMenu from "../context_menus/MessageContextMenu"; import { aboveRightOf } from '../../structures/ContextMenu'; import { objectHasDiff } from "../../../utils/objects"; -import Tooltip from "../elements/Tooltip"; +import Tooltip, { Alignment } from "../elements/Tooltip"; import EditorStateTransfer from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; @@ -54,7 +53,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import SenderProfile from '../messages/SenderProfile'; import MessageTimestamp from '../messages/MessageTimestamp'; import TooltipButton from '../elements/TooltipButton'; -import ReadReceiptMarker, { IReadReceiptInfo } from "./ReadReceiptMarker"; +import { IReadReceiptInfo } from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventRenderingUtils'; @@ -79,6 +78,8 @@ import PosthogTrackers from "../../../PosthogTrackers"; import TileErrorBoundary from '../messages/TileErrorBoundary'; import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../events/EventTileFactory"; import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary"; +import { ReadReceiptGroup } from './ReadReceiptGroup'; +import { useTooltip } from "../../../utils/useTooltip"; export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations; @@ -221,9 +222,6 @@ interface IProps { interface IState { // Whether the action bar is focused. actionBarFocused: boolean; - // Whether all read receipts are being displayed. If not, only display - // a truncation of them. - allReadAvatars: boolean; // Whether the event's sender has been verified. verified: string; // Whether onRequestKeysClick has been called since mounting. @@ -273,9 +271,6 @@ export class UnwrappedEventTile extends React.Component { this.state = { // Whether the action bar is focused. actionBarFocused: false, - // Whether all read receipts are being displayed. If not, only display - // a truncation of them. - allReadAvatars: false, // Whether the event's sender has been verified. verified: null, // Whether onRequestKeysClick has been called since mounting. @@ -731,108 +726,6 @@ export class UnwrappedEventTile extends React.Component { return actions.tweaks.highlight; } - private toggleAllReadAvatars = () => { - this.setState({ - allReadAvatars: !this.state.allReadAvatars, - }); - }; - - private getReadAvatars() { - if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { - return ; - } - - const MAX_READ_AVATARS = this.props.layout == Layout.Bubble - ? 2 - : 5; - - // return early if there are no read receipts - if (!this.props.readReceipts || this.props.readReceipts.length === 0) { - // We currently must include `mx_EventTile_readAvatars` in the DOM - // of all events, as it is the positioned parent of the animated - // read receipts. We can't let it unmount when a receipt moves - // events, so for now we mount it for all events. Without it, the - // animation will start from the top of the timeline (because it - // lost its container). - // See also https://github.com/vector-im/element-web/issues/17561 - return ( -
    - -
    - ); - } - - const avatars = []; - const receiptOffset = 15; - let left = 0; - - const receipts = this.props.readReceipts; - - for (let i = 0; i < receipts.length; ++i) { - const receipt = receipts[i]; - - let hidden = true; - if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) { - hidden = false; - } - // TODO: we keep the extra read avatars in the dom to make animation simpler - // we could optimise this to reduce the dom size. - - // If hidden, set offset equal to the offset of the final visible avatar or - // else set it proportional to index - left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset; - - const userId = receipt.userId; - let readReceiptInfo: IReadReceiptInfo; - - if (this.props.readReceiptMap) { - readReceiptInfo = this.props.readReceiptMap[userId]; - if (!readReceiptInfo) { - readReceiptInfo = {}; - this.props.readReceiptMap[userId] = readReceiptInfo; - } - } - - // add to the start so the most recent is on the end (ie. ends up rightmost) - avatars.unshift( -