diff --git a/CHANGELOG.md b/CHANGELOG.md index d459b4e94a..58d23e3413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,115 @@ +Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0) + + * Upgrade to JS SDK 10.1.0 + * [Release] Don't use the event's metadata to calc the scale of an image + [\#6004](https://github.com/matrix-org/matrix-react-sdk/pull/6004) + +Changes in [3.20.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0-rc.1) (2021-05-04) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0...v3.20.0-rc.1) + + * Upgrade to JS SDK 10.1.0-rc.1 + * Translations update from Weblate + [\#5966](https://github.com/matrix-org/matrix-react-sdk/pull/5966) + * Fix more space panel layout and hover behaviour issues + [\#5965](https://github.com/matrix-org/matrix-react-sdk/pull/5965) + * Fix edge case with space panel alignment with subspaces on ff + [\#5964](https://github.com/matrix-org/matrix-react-sdk/pull/5964) + * Fix saving room pill part to history + [\#5951](https://github.com/matrix-org/matrix-react-sdk/pull/5951) + * Generate room preview even when minimized + [\#5948](https://github.com/matrix-org/matrix-react-sdk/pull/5948) + * Another change from recovery passphrase to Security Phrase + [\#5934](https://github.com/matrix-org/matrix-react-sdk/pull/5934) + * Sort rooms in the add existing to space dialog based on recency + [\#5943](https://github.com/matrix-org/matrix-react-sdk/pull/5943) + * Inhibit sending RR when context switching to a room + [\#5944](https://github.com/matrix-org/matrix-react-sdk/pull/5944) + * Prevent room list keyboard handling from landing focus on hidden nodes + [\#5950](https://github.com/matrix-org/matrix-react-sdk/pull/5950) + * Make the text filter search all spaces instead of just the selected one + [\#5942](https://github.com/matrix-org/matrix-react-sdk/pull/5942) + * Enable indent rule and fix indent + [\#5931](https://github.com/matrix-org/matrix-react-sdk/pull/5931) + * Prevent peeking members from reacting + [\#5946](https://github.com/matrix-org/matrix-react-sdk/pull/5946) + * Disallow inline display maths + [\#5939](https://github.com/matrix-org/matrix-react-sdk/pull/5939) + * Space creation prompt user to add existing rooms for "Just Me" spaces + [\#5923](https://github.com/matrix-org/matrix-react-sdk/pull/5923) + * Add test coverage collection script + [\#5937](https://github.com/matrix-org/matrix-react-sdk/pull/5937) + * Fix joining room using via servers regression + [\#5936](https://github.com/matrix-org/matrix-react-sdk/pull/5936) + * Revert "Fixes the two Todays problem in Redaction" + [\#5938](https://github.com/matrix-org/matrix-react-sdk/pull/5938) + * Handle encoded matrix URLs + [\#5903](https://github.com/matrix-org/matrix-react-sdk/pull/5903) + * Render ignored users setting regardless of if there are any + [\#5860](https://github.com/matrix-org/matrix-react-sdk/pull/5860) + * Fix inserting trailing colon after mention/pill + [\#5830](https://github.com/matrix-org/matrix-react-sdk/pull/5830) + * Fixes the two Todays problem in Redaction + [\#5917](https://github.com/matrix-org/matrix-react-sdk/pull/5917) + * Fix page up/down scrolling only half a page + [\#5920](https://github.com/matrix-org/matrix-react-sdk/pull/5920) + * Voice messages: Composer controls + [\#5935](https://github.com/matrix-org/matrix-react-sdk/pull/5935) + * Support MSC3086 asserted identity + [\#5886](https://github.com/matrix-org/matrix-react-sdk/pull/5886) + * Handle possible edge case with getting stuck in "unsent messages" bar + [\#5930](https://github.com/matrix-org/matrix-react-sdk/pull/5930) + * Fix suggested rooms not showing up regression from room list optimisation + [\#5932](https://github.com/matrix-org/matrix-react-sdk/pull/5932) + * Broadcast language change to ElectronPlatform + [\#5913](https://github.com/matrix-org/matrix-react-sdk/pull/5913) + * Fix VoIP PIP frame color + [\#5701](https://github.com/matrix-org/matrix-react-sdk/pull/5701) + * Convert some Flow-typed files to TypeScript + [\#5912](https://github.com/matrix-org/matrix-react-sdk/pull/5912) + * Initial SpaceStore tests work + [\#5906](https://github.com/matrix-org/matrix-react-sdk/pull/5906) + * Fix issues with space hierarchy in layout and with incompatible servers + [\#5926](https://github.com/matrix-org/matrix-react-sdk/pull/5926) + * Scale all mxc thumbs using device pixel ratio for hidpi + [\#5928](https://github.com/matrix-org/matrix-react-sdk/pull/5928) + * Fix add existing to space dialog no longer showing rooms for public spaces + [\#5918](https://github.com/matrix-org/matrix-react-sdk/pull/5918) + * Disable spaces context switching for when exploring a space + [\#5924](https://github.com/matrix-org/matrix-react-sdk/pull/5924) + * Autofocus search box in the add existing to space dialog + [\#5921](https://github.com/matrix-org/matrix-react-sdk/pull/5921) + * Use label element in add existing to space dialog for easier hit target + [\#5922](https://github.com/matrix-org/matrix-react-sdk/pull/5922) + * Dynamic max and min zoom in the new ImageView + [\#5916](https://github.com/matrix-org/matrix-react-sdk/pull/5916) + * Improve message error states + [\#5897](https://github.com/matrix-org/matrix-react-sdk/pull/5897) + * Check for null room in `VisibilityProvider` + [\#5914](https://github.com/matrix-org/matrix-react-sdk/pull/5914) + * Add unit tests for various collection-based utility functions + [\#5910](https://github.com/matrix-org/matrix-react-sdk/pull/5910) + * Spaces visual fixes + [\#5909](https://github.com/matrix-org/matrix-react-sdk/pull/5909) + * Remove reliance on DOM API to generated message preview + [\#5908](https://github.com/matrix-org/matrix-react-sdk/pull/5908) + * Expand upon voice message event & include overall waveform + [\#5888](https://github.com/matrix-org/matrix-react-sdk/pull/5888) + * Use floats for image background opacity + [\#5905](https://github.com/matrix-org/matrix-react-sdk/pull/5905) + * Show invites to spaces at the top of the space panel + [\#5902](https://github.com/matrix-org/matrix-react-sdk/pull/5902) + * Improve edge cases with spaces context switching + [\#5899](https://github.com/matrix-org/matrix-react-sdk/pull/5899) + * Fix spaces notification dots wrongly including upgraded (hidden) rooms + [\#5900](https://github.com/matrix-org/matrix-react-sdk/pull/5900) + * Iterate the spaces face pile design + [\#5898](https://github.com/matrix-org/matrix-react-sdk/pull/5898) + * Fix alignment issue with nested spaces being cut off wrong + [\#5890](https://github.com/matrix-org/matrix-react-sdk/pull/5890) + Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0) diff --git a/package.json b/package.json index be195e2e9e..342591081f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.19.0", + "version": "3.20.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -97,7 +97,7 @@ "react-transition-group": "^4.4.1", "resize-observer-polyfill": "^1.5.1", "rfc4648": "^1.4.0", - "sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db", + "sanitize-html": "^2.3.2", "tar-js": "^0.3.0", "text-encoding-utf-8": "^1.0.2", "url": "^0.11.0", diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index b87211a847..a3ee104bd8 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -46,11 +46,11 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/trashcan.svg'); } -.mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer { +.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer { // Note: remaining class properties are in the PlayerContainer CSS. margin: 6px; // force the composer area to put a gutter around us - margin-right: 12px; // isolate from stop button + margin-right: 12px; // isolate from stop/send button position: relative; // important for the live circle diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 34682877e0..bda46aef07 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -27,8 +27,8 @@ import { Action } from "../../dispatcher/actions"; import RoomListStore from "../../stores/room-list/RoomListStore"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; -import {replaceableComponent} from "../../utils/replaceableComponent"; -import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore"; interface IProps { isMinimized: boolean; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 35efd12c9c..365f2ab1de 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -78,8 +78,10 @@ export default class MessageContextMenu extends React.Component { // We explicitly decline to show the redact option on ACL events as it has a potential // to obliterate the room - https://github.com/matrix-org/synapse/issues/4042 + // Similarly for encryption events, since redacting them "breaks everything" const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId) - && this.props.mxEvent.getType() !== EventType.RoomServerAcl; + && this.props.mxEvent.getType() !== EventType.RoomServerAcl + && this.props.mxEvent.getType() !== EventType.RoomEncryption; let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality diff --git a/src/components/views/dialogs/UploadConfirmDialog.js b/src/components/views/dialogs/UploadConfirmDialog.tsx similarity index 72% rename from src/components/views/dialogs/UploadConfirmDialog.js rename to src/components/views/dialogs/UploadConfirmDialog.tsx index 2ff16b9440..7f6bcd27d1 100644 --- a/src/components/views/dialogs/UploadConfirmDialog.js +++ b/src/components/views/dialogs/UploadConfirmDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,20 +16,23 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import filesize from "filesize"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { getBlobSafeMimeType } from '../../../utils/blobs'; + +interface IProps { + file: File; + currentIndex: number; + totalFiles?: number; + onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void; +} @replaceableComponent("views.dialogs.UploadConfirmDialog") -export default class UploadConfirmDialog extends React.Component { - static propTypes = { - file: PropTypes.object.isRequired, - currentIndex: PropTypes.number, - totalFiles: PropTypes.number, - onFinished: PropTypes.func.isRequired, - } +export default class UploadConfirmDialog extends React.Component { + private objectUrl: string; + private mimeType: string; static defaultProps = { totalFiles: 1, @@ -38,22 +41,28 @@ export default class UploadConfirmDialog extends React.Component { constructor(props) { super(props); - this._objectUrl = URL.createObjectURL(props.file); + // Create a fresh `Blob` for previewing (even though `File` already is + // one) so we can adjust the MIME type if needed. + this.mimeType = getBlobSafeMimeType(props.file.type); + const blob = new Blob([props.file], { type: + this.mimeType, + }); + this.objectUrl = URL.createObjectURL(blob); } componentWillUnmount() { - if (this._objectUrl) URL.revokeObjectURL(this._objectUrl); + if (this.objectUrl) URL.revokeObjectURL(this.objectUrl); } - _onCancelClick = () => { + private onCancelClick = () => { this.props.onFinished(false); } - _onUploadClick = () => { + private onUploadClick = () => { this.props.onFinished(true); } - _onUploadAllClick = () => { + private onUploadAllClick = () => { this.props.onFinished(true, true); } @@ -75,10 +84,10 @@ export default class UploadConfirmDialog extends React.Component { } let preview; - if (this.props.file.type.startsWith('image/')) { + if (this.mimeType.startsWith('image/')) { preview =
-
+
{this.props.file.name} ({filesize(this.props.file.size)})
; @@ -95,7 +104,7 @@ export default class UploadConfirmDialog extends React.Component { let uploadAllButton; if (this.props.currentIndex + 1 < this.props.totalFiles) { - uploadAllButton = ; } @@ -103,7 +112,7 @@ export default class UploadConfirmDialog extends React.Component { return ( @@ -113,7 +122,7 @@ export default class UploadConfirmDialog extends React.Component { {uploadAllButton} diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 05d487a9eb..f736736acc 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -108,8 +108,6 @@ export default class ImageView extends React.Component { window.addEventListener("resize", this.calculateZoom); // After the image loads for the first time we want to calculate the zoom this.image.current.addEventListener("load", this.calculateZoom); - // Try to precalculate the zoom from width and height props - this.calculateZoom(); } componentWillUnmount() { @@ -122,11 +120,8 @@ export default class ImageView extends React.Component { const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const width = this.props.width || image.naturalWidth; - const height = this.props.height || image.naturalHeight; - - const zoomX = imageWrapper.clientWidth / width; - const zoomY = imageWrapper.clientHeight / height; + const zoomX = imageWrapper.clientWidth / image.naturalWidth; + const zoomY = imageWrapper.clientHeight / image.naturalHeight; // If the image is smaller in both dimensions set its the zoom to 1 to // display it in its original size diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 2fa5fd89d3..faf8860cad 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -187,9 +187,15 @@ function DeviceItem({userId, device}: {userId: string, device: IDevice}) { verifyDevice(cli.getUser(userId), device); }; - const deviceName = device.ambiguous ? - (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : - device.getDisplayName(); + let deviceName; + if (!device.getDisplayName()?.trim()) { + deviceName = device.deviceId; + } else { + deviceName = device.ambiguous ? + device.getDisplayName() + " (" + device.deviceId + ")" : + device.getDisplayName(); + } + let trustedLabel = null; if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 36ab423885..411b0f9b5e 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -26,13 +26,11 @@ import {SpaceItem} from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import SpaceStore, { - HOME_SPACE, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../../stores/SpaceStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; import { RovingAccessibleButton, @@ -40,13 +38,15 @@ import { RovingTabIndexProvider, } from "../../../accessibility/RovingTabIndex"; import {Key} from "../../../Keyboard"; +import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore"; +import {NotificationState} from "../../../stores/notifications/NotificationState"; interface IButtonProps { space?: Room; className?: string; selected?: boolean; tooltip?: string; - notificationState?: SpaceNotificationState; + notificationState?: NotificationState; isNarrow?: boolean; onClick(): void; } @@ -212,8 +212,8 @@ const SpacePanel = () => { className="mx_SpaceButton_home" onClick={() => SpaceStore.instance.setActiveSpace(null)} selected={!activeSpace} - tooltip={_t("Home")} - notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)} + tooltip={_t("All rooms")} + notificationState={RoomNotificationStateStore.instance.globalState} isNarrow={isPanelCollapsed} /> { invites.map(s => %(serverName)s": "Are you sure you want to remove %(serverName)s", "Remove server": "Remove server", diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 4423891c61..b1b8199f93 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -31,28 +31,23 @@ import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateS import {DefaultTagID} from "./room-list/models"; import {EnhancedMap, mapDiff} from "../utils/maps"; import {setHasDiff} from "../utils/sets"; -import {objectDiff} from "../utils/objects"; -import {arrayHasDiff} from "../utils/arrays"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; -type SpaceKey = string | symbol; - interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; -export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); -// Space Room ID/HOME_SPACE will be emitted when a Space's children change +// Space Room ID will be emitted when a Space's children change const MAX_SUGGESTED_ROOMS = 20; -const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "home_space"}`; +const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -86,15 +81,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // The spaces representing the roots of the various tree-like hierarchies private rootSpaces: Room[] = []; - // The list of rooms not present in any currently joined spaces - private orphanedRooms = new Set(); // Map from room ID to set of spaces which list it as a child private parentMap = new EnhancedMap>(); - // Map from space key to SpaceNotificationState instance representing that space - private notificationStateMap = new Map(); + // Map from spaceId to SpaceNotificationState instance representing that space + private notificationStateMap = new Map(); // Map from space key to Set of room IDs that should be shown as part of that space's filter - private spaceFilteredRooms = new Map>(); - // The space currently selected in the Space Panel - if null then `Home` is selected + private spaceFilteredRooms = new Map>(); + // The space currently selected in the Space Panel - if null then All Rooms is selected private _activeSpace?: Room = null; private _suggestedRooms: ISpaceSummaryRoom[] = []; private _invitedSpaces = new Set(); @@ -244,7 +237,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getSpaceFilteredRoomIds = (space: Room | null): Set => { - return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); + if (!space) { + return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); + } + return this.spaceFilteredRooms.get(space.roomId) || new Set(); }; private rebuild = throttle(() => { @@ -275,7 +271,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }); - const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); + const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren)); // somewhat algorithm to handle full-cycles const detachedNodes = new Set(spaces); @@ -316,7 +312,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.orphanedRooms = new Set(orphanedRooms); this.rootSpaces = rootSpaces; this.parentMap = backrefs; @@ -337,25 +332,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.rebuild(); } - private showInHomeSpace = (room: Room) => { - if (room.isSpaceRoom()) return false; - return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space - || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space - || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites - }; - - // Update a given room due to its tag changing (e.g DM-ness or Fav-ness) - // This can only change whether it shows up in the HOME_SPACE or not - private onRoomUpdate = (room: Room) => { - if (this.showInHomeSpace(room)) { - this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); - this.emit(HOME_SPACE); - } else if (!this.orphanedRooms.has(room.roomId)) { - this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); - this.emit(HOME_SPACE); - } - }; - private onSpaceMembersChange = (ev: MatrixEvent) => { // skip this update if we do not have a DM with this user if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return; @@ -369,16 +345,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - // put all room invites in the Home Space - const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); - this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); - - visibleRooms.forEach(room => { - if (this.showInHomeSpace(room)) { - this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId); - } - }); - this.rootSpaces.forEach(s => { // traverse each space tree in DFS to build up the supersets as you go up, // reusing results from like subtrees. @@ -425,13 +391,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Update NotificationStates this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { if (roomIds.has(room.roomId)) { - // Don't aggregate notifications for DMs except in the Home Space - if (s !== HOME_SPACE) { - return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) - || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); - } - - return true; + return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) + || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); } return false; @@ -513,8 +474,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // TODO confirm this after implementing parenting behaviour if (room.isSpaceRoom()) { this.onSpaceUpdate(); - } else { - this.onRoomUpdate(room); } this.emit(room.roomId); break; @@ -527,38 +486,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; - private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => { - if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { - // If the room was in favourites and now isn't or the opposite then update its position in the trees - const oldTags = lastEvent?.getContent()?.tags || {}; - const newTags = ev.getContent()?.tags || {}; - if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { - this.onRoomUpdate(room); - } - } - } - - private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { - if (ev.getType() === EventType.Direct) { - const lastContent = lastEvent.getContent(); - const content = ev.getContent(); - - const diff = objectDiff>(lastContent, content); - // filter out keys which changed by reference only by checking whether the sets differ - const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k])); - // DM tag changes, refresh relevant rooms - new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => { - const room = this.matrixClient?.getRoom(roomId); - if (room) { - this.onRoomUpdate(room); - } - }); - } - }; - protected async reset() { this.rootSpaces = []; - this.orphanedRooms = new Set(); this.parentMap = new EnhancedMap(); this.notificationStateMap = new Map(); this.spaceFilteredRooms = new Map(); @@ -573,8 +502,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room", this.onRoom); this.matrixClient.removeListener("Room.myMembership", this.onRoom); this.matrixClient.removeListener("RoomState.events", this.onRoomState); - this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); - this.matrixClient.removeListener("accountData", this.onAccountData); } await this.reset(); } @@ -584,8 +511,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("Room", this.onRoom); this.matrixClient.on("Room.myMembership", this.onRoom); this.matrixClient.on("RoomState.events", this.onRoomState); - this.matrixClient.on("Room.accountData", this.onRoomAccountData); - this.matrixClient.on("accountData", this.onAccountData); await this.onSpaceUpdate(); // trigger an initial update @@ -610,7 +535,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Don't context switch when navigating to the space room // as it will cause you to end up in the wrong room this.setActiveSpace(room, false); - } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { + } else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { this.switchToRelatedSpace(roomId); } @@ -628,7 +553,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } } - public getNotificationState(key: SpaceKey): SpaceNotificationState { + public getNotificationState(key: string): SpaceNotificationState { if (this.notificationStateMap.has(key)) { return this.notificationStateMap.get(key); } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 58eb6ed317..a23401e4c9 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -680,7 +680,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { promise = this.recalculatePrefiltering(); } else { this.filterConditions.push(filter); - // Runtime filters with spaces disable prefiltering for the search all spaces effect + // Runtime filters with spaces disable prefiltering for the search all spaces feature if (SettingsStore.getValue("feature_spaces")) { // this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below // this way the runtime filters are only evaluated on one dataset and not both. @@ -712,10 +712,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (this.algorithm) { this.algorithm.removeFilterCondition(filter); - // Runtime filters with spaces disable prefiltering for the search all spaces effect - if (SettingsStore.getValue("feature_spaces")) { - promise = this.recalculatePrefiltering(); - } + } + // Runtime filters with spaces disable prefiltering for the search all spaces feature + if (SettingsStore.getValue("feature_spaces")) { + promise = this.recalculatePrefiltering(); } } idx = this.prefilterConditions.indexOf(filter); diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index 13e1d83901..0b1b78bc75 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -24,26 +24,34 @@ import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; * Watches for changes in spaces to manage the filter on the provided RoomListStore */ export class SpaceWatcher { - private filter = new SpaceFilterCondition(); + private filter: SpaceFilterCondition; private activeSpace: Room = SpaceStore.instance.activeSpace; constructor(private store: RoomListStoreClass) { - this.updateFilter(); // get the filter into a consistent state - store.addFilter(this.filter); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); } - private onSelectedSpaceUpdated = (activeSpace: Room) => { + private onSelectedSpaceUpdated = (activeSpace?: Room) => { this.activeSpace = activeSpace; - this.updateFilter(); + + if (this.filter) { + if (activeSpace) { + this.updateFilter(); + } else { + this.store.removeFilter(this.filter); + this.filter = null; + } + } else if (activeSpace) { + this.filter = new SpaceFilterCondition(); + this.updateFilter(); + this.store.addFilter(this.filter); + } }; private updateFilter = () => { - if (this.activeSpace) { - SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { - this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); - }); - } + SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { + this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); + }); this.filter.updateSpace(this.activeSpace); }; } diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 43bdcb3879..6a06bee0d8 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { IDestroyable } from "../../../utils/IDestroyable"; -import SpaceStore, {HOME_SPACE} from "../../SpaceStore"; +import SpaceStore from "../../SpaceStore"; import { setHasDiff } from "../../../utils/sets"; /** @@ -55,10 +55,12 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi } }; - private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE; + private getSpaceEventKey = (space: Room) => space.roomId; public updateSpace(space: Room) { - SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); + if (this.space) { + SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); + } SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate); this.onStoreUpdate(); // initial update from the change to the space } diff --git a/src/utils/DecryptFile.ts b/src/utils/DecryptFile.ts index 93cedbc707..d073393170 100644 --- a/src/utils/DecryptFile.ts +++ b/src/utils/DecryptFile.ts @@ -17,63 +17,8 @@ limitations under the License. // Pull in the encryption lib so that we can decrypt attachments. import encrypt from 'browser-encrypt-attachment'; import {mediaFromContent} from "../customisations/Media"; -import {IEncryptedFile} from "../customisations/models/IMediaEventContent"; - -// WARNING: We have to be very careful about what mime-types we allow into blobs, -// as for performance reasons these are now rendered via URL.createObjectURL() -// rather than by converting into data: URIs. -// -// This means that the content is rendered using the origin of the script which -// called createObjectURL(), and so if the content contains any scripting then it -// will pose a XSS vulnerability when the browser renders it. This is particularly -// bad if the user right-clicks the URI and pastes it into a new window or tab, -// as the blob will then execute with access to Element's full JS environment(!) -// -// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647 -// for details. -// -// We mitigate this by only allowing mime-types into blobs which we know don't -// contain any scripting, and instantiate all others as application/octet-stream -// regardless of what mime-type the event claimed. Even if the payload itself -// is some malicious HTML, the fact we instantiate it with a media mimetype or -// application/octet-stream means the browser doesn't try to render it as such. -// -// One interesting edge case is image/svg+xml, which empirically *is* rendered -// correctly if the blob is set to the src attribute of an img tag (for thumbnails) -// *even if the mimetype is application/octet-stream*. However, empirically JS -// in the SVG isn't executed in this scenario, so we seem to be okay. -// -// Tested on Chrome 65 and Firefox 60 -// -// The list below is taken mainly from -// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats -// N.B. Matrix doesn't currently specify which mimetypes are valid in given -// events, so we pick the ones which HTML5 browsers should be able to display -// -// For the record, mime-types which must NEVER enter this list below include: -// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar. - -const ALLOWED_BLOB_MIMETYPES = [ - 'image/jpeg', - 'image/gif', - 'image/png', - - 'video/mp4', - 'video/webm', - 'video/ogg', - - 'audio/mp4', - 'audio/webm', - 'audio/aac', - 'audio/mpeg', - 'audio/ogg', - 'audio/wave', - 'audio/wav', - 'audio/x-wav', - 'audio/x-pn-wav', - 'audio/flac', - 'audio/x-flac', -]; +import { IEncryptedFile } from "../customisations/models/IMediaEventContent"; +import { getBlobSafeMimeType } from "./blobs"; /** * Decrypt a file attached to a matrix event. @@ -100,9 +45,7 @@ export function decryptFile(file: IEncryptedFile): Promise { // browser (e.g. by copying the URI into a new tab or window.) // See warning at top of file. let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : ''; - if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { - mimetype = 'application/octet-stream'; - } + mimetype = getBlobSafeMimeType(mimetype); return new Blob([dataArray], {type: mimetype}); }); diff --git a/src/utils/blobs.ts b/src/utils/blobs.ts new file mode 100644 index 0000000000..4e073a3936 --- /dev/null +++ b/src/utils/blobs.ts @@ -0,0 +1,78 @@ +/* +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. +*/ + +// WARNING: We have to be very careful about what mime-types we allow into blobs, +// as for performance reasons these are now rendered via URL.createObjectURL() +// rather than by converting into data: URIs. +// +// This means that the content is rendered using the origin of the script which +// called createObjectURL(), and so if the content contains any scripting then it +// will pose a XSS vulnerability when the browser renders it. This is particularly +// bad if the user right-clicks the URI and pastes it into a new window or tab, +// as the blob will then execute with access to Element's full JS environment(!) +// +// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647 +// for details. +// +// We mitigate this by only allowing mime-types into blobs which we know don't +// contain any scripting, and instantiate all others as application/octet-stream +// regardless of what mime-type the event claimed. Even if the payload itself +// is some malicious HTML, the fact we instantiate it with a media mimetype or +// application/octet-stream means the browser doesn't try to render it as such. +// +// One interesting edge case is image/svg+xml, which empirically *is* rendered +// correctly if the blob is set to the src attribute of an img tag (for thumbnails) +// *even if the mimetype is application/octet-stream*. However, empirically JS +// in the SVG isn't executed in this scenario, so we seem to be okay. +// +// Tested on Chrome 65 and Firefox 60 +// +// The list below is taken mainly from +// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats +// N.B. Matrix doesn't currently specify which mimetypes are valid in given +// events, so we pick the ones which HTML5 browsers should be able to display +// +// For the record, mime-types which must NEVER enter this list below include: +// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar. + +const ALLOWED_BLOB_MIMETYPES = [ + 'image/jpeg', + 'image/gif', + 'image/png', + + 'video/mp4', + 'video/webm', + 'video/ogg', + + 'audio/mp4', + 'audio/webm', + 'audio/aac', + 'audio/mpeg', + 'audio/ogg', + 'audio/wave', + 'audio/wav', + 'audio/x-wav', + 'audio/x-pn-wav', + 'audio/flac', + 'audio/x-flac', +]; + +export function getBlobSafeMimeType(mimetype: string): string { + if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { + return 'application/octet-stream'; + } + return mimetype; +} diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index aef788647d..20c48c29db 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -101,6 +101,7 @@ const invite1 = "!invite1:server"; const invite2 = "!invite2:server"; const room1 = "!room1:server"; const room2 = "!room2:server"; +const room3 = "!room3:server"; const space1 = "!space1:server"; const space2 = "!space2:server"; const space3 = "!space3:server"; @@ -361,8 +362,8 @@ describe("SpaceStore", () => { expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy(); }); - it("home space does not contain rooms/low priority from rooms within spaces", () => { - expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); + it("home space does contain rooms/low priority even if they are also shown in a space", () => { + expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy(); }); it("space contains child rooms", () => { @@ -614,8 +615,8 @@ describe("SpaceStore", () => { describe("space auto switching tests", () => { beforeEach(async () => { - [room1, room2, orphan1].forEach(mkRoom); - mkSpace(space1, [room1, room2]); + [room1, room2, room3, orphan1].forEach(mkRoom); + mkSpace(space1, [room1, room2, room3]); mkSpace(space2, [room1, room2]); client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([ @@ -641,15 +642,15 @@ describe("SpaceStore", () => { it("switch to canonical parent space for room", async () => { viewRoom(room1); - await store.setActiveSpace(null, false); + await store.setActiveSpace(client.getRoom(space2), false); viewRoom(room2); expect(store.activeSpace).toBe(client.getRoom(space2)); }); it("switch to first containing space for room", async () => { viewRoom(room2); - await store.setActiveSpace(null, false); - viewRoom(room1); + await store.setActiveSpace(client.getRoom(space2), false); + viewRoom(room3); expect(store.activeSpace).toBe(client.getRoom(space1)); }); @@ -659,6 +660,13 @@ describe("SpaceStore", () => { viewRoom(orphan1); expect(store.activeSpace).toBeNull(); }); + + it("when switching rooms in the all rooms home space don't switch to related space", async () => { + viewRoom(room2); + await store.setActiveSpace(null, false); + viewRoom(room1); + expect(store.activeSpace).toBeNull(); + }); }); describe("traverseSpace", () => { diff --git a/yarn.lock b/yarn.lock index 8b4ac35d6a..0d9b2bdd3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4226,9 +4226,9 @@ hoist-non-react-statics@^3.3.0: react-is "^16.7.0" hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hosted-git-info@^3.0.6: version "3.0.7" @@ -5677,8 +5677,8 @@ mathml-tag-names@^2.1.3: integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "10.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f69c0b7937b9064938c134d708c4d064b71315" + version "10.1.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2d73805ca3d8c5a140fe05e574f826696de1656a" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -6307,6 +6307,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + parse5-htmlparser2-tree-adapter@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" @@ -7248,17 +7253,18 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -"sanitize-html@github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db": - version "2.0.0-rc.3" - resolved "https://codeload.github.com/apostrophecms/sanitize-html/tar.gz/3c7f93f2058f696f5359e3e58d464161647226db" +sanitize-html@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353" + integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" - htmlparser2 "^4.1.0" + htmlparser2 "^6.0.0" is-plain-object "^5.0.0" klona "^2.0.3" + parse-srcset "^1.0.2" postcss "^8.0.2" - srcset "^3.0.0" saxes@^5.0.0: version "5.0.1" @@ -7515,11 +7521,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -srcset@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-3.0.0.tgz#8afd8b971362dfc129ae9c1a99b3897301ce6441" - integrity sha512-D59vF08Qzu/C4GAOXVgMTLfgryt5fyWo93FZyhEWANo0PokFz/iWdDe13mX3O5TRf6l8vMTqckAfR4zPiaH0yQ== - sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"