diff --git a/CHANGELOG.md b/CHANGELOG.md index c31eedf93b..38b1a2572f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,126 @@ +Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0) + + * Upgrade to JS SDK 9.9.0 + * [Release] Change read receipt drift to be non-fractional + [\#5746](https://github.com/matrix-org/matrix-react-sdk/pull/5746) + * [Release] Properly gate SpaceRoomView behind labs + [\#5750](https://github.com/matrix-org/matrix-react-sdk/pull/5750) + +Changes in [3.16.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.2) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.1...v3.16.0-rc.2) + + * Fixed incorrect build output in rc.1 + +Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1) + + * Upgrade to JS SDK 9.9.0-rc.1 + * Translations update from Weblate + [\#5743](https://github.com/matrix-org/matrix-react-sdk/pull/5743) + * Document behaviour of showReadReceipts=false for sent receipts + [\#5739](https://github.com/matrix-org/matrix-react-sdk/pull/5739) + * Tweak sent marker code style + [\#5741](https://github.com/matrix-org/matrix-react-sdk/pull/5741) + * Fix sent markers disappearing for edits/reactions + [\#5737](https://github.com/matrix-org/matrix-react-sdk/pull/5737) + * Ignore to-device decryption in the room list store + [\#5740](https://github.com/matrix-org/matrix-react-sdk/pull/5740) + * Spaces suggested rooms support + [\#5736](https://github.com/matrix-org/matrix-react-sdk/pull/5736) + * Add tooltips to sent/sending receipts + [\#5738](https://github.com/matrix-org/matrix-react-sdk/pull/5738) + * Remove a bunch of useless 'use strict' definitions + [\#5735](https://github.com/matrix-org/matrix-react-sdk/pull/5735) + * [SK-1] Fix types for replaceableComponent + [\#5732](https://github.com/matrix-org/matrix-react-sdk/pull/5732) + * [SK-2] Make debugging skinning problems easier + [\#5733](https://github.com/matrix-org/matrix-react-sdk/pull/5733) + * Support sending invite reasons with /invite command + [\#5695](https://github.com/matrix-org/matrix-react-sdk/pull/5695) + * Fix clicking on the avatar for opening member info requires pixel-perfect + accuracy + [\#5717](https://github.com/matrix-org/matrix-react-sdk/pull/5717) + * Display decrypted and encrypted event source on the same dialog + [\#5713](https://github.com/matrix-org/matrix-react-sdk/pull/5713) + * Fix units of TURN server expiry time + [\#5730](https://github.com/matrix-org/matrix-react-sdk/pull/5730) + * Display room name in pills instead of address + [\#5624](https://github.com/matrix-org/matrix-react-sdk/pull/5624) + * Refresh UI for file uploads + [\#5723](https://github.com/matrix-org/matrix-react-sdk/pull/5723) + * UI refresh for uploaded files + [\#5719](https://github.com/matrix-org/matrix-react-sdk/pull/5719) + * Improve message sending states to match new designs + [\#5699](https://github.com/matrix-org/matrix-react-sdk/pull/5699) + * Add clipboard write permission for widgets + [\#5725](https://github.com/matrix-org/matrix-react-sdk/pull/5725) + * Fix widget resizing + [\#5722](https://github.com/matrix-org/matrix-react-sdk/pull/5722) + * Option for audio streaming + [\#5707](https://github.com/matrix-org/matrix-react-sdk/pull/5707) + * Show a specific error for hs_disabled + [\#5576](https://github.com/matrix-org/matrix-react-sdk/pull/5576) + * Add Edge to the targets list + [\#5721](https://github.com/matrix-org/matrix-react-sdk/pull/5721) + * File drop UI fixes and improvements + [\#5505](https://github.com/matrix-org/matrix-react-sdk/pull/5505) + * Fix Bottom border of state counters is white on the dark theme + [\#5715](https://github.com/matrix-org/matrix-react-sdk/pull/5715) + * Trim spurious whitespace of nicknames + [\#5332](https://github.com/matrix-org/matrix-react-sdk/pull/5332) + * Ensure HostSignupDialog border colour matches light theme + [\#5716](https://github.com/matrix-org/matrix-react-sdk/pull/5716) + * Don't place another call if there's already one ongoing + [\#5712](https://github.com/matrix-org/matrix-react-sdk/pull/5712) + * Space room hierarchies + [\#5706](https://github.com/matrix-org/matrix-react-sdk/pull/5706) + * Iterate Space view and right panel + [\#5705](https://github.com/matrix-org/matrix-react-sdk/pull/5705) + * Add a scroll to bottom on message sent setting + [\#5692](https://github.com/matrix-org/matrix-react-sdk/pull/5692) + * Add .tmp files to gitignore + [\#5708](https://github.com/matrix-org/matrix-react-sdk/pull/5708) + * Initial Space Room View and Creation UX + [\#5704](https://github.com/matrix-org/matrix-react-sdk/pull/5704) + * Add multi language spell check + [\#5452](https://github.com/matrix-org/matrix-react-sdk/pull/5452) + * Fix tetris effect (holes) in read receipts + [\#5697](https://github.com/matrix-org/matrix-react-sdk/pull/5697) + * Fixed edit for markdown images + [\#5703](https://github.com/matrix-org/matrix-react-sdk/pull/5703) + * Iterate Space Panel + [\#5702](https://github.com/matrix-org/matrix-react-sdk/pull/5702) + * Fix read receipts for compact layout + [\#5700](https://github.com/matrix-org/matrix-react-sdk/pull/5700) + * Space Store and Space Panel for Room List filtering + [\#5689](https://github.com/matrix-org/matrix-react-sdk/pull/5689) + * Log when turn creds expire + [\#5691](https://github.com/matrix-org/matrix-react-sdk/pull/5691) + * Null check for maxHeight in call view + [\#5690](https://github.com/matrix-org/matrix-react-sdk/pull/5690) + * Autocomplete invited users + [\#5687](https://github.com/matrix-org/matrix-react-sdk/pull/5687) + * Add send message button + [\#5535](https://github.com/matrix-org/matrix-react-sdk/pull/5535) + * Move call buttons to the room header + [\#5693](https://github.com/matrix-org/matrix-react-sdk/pull/5693) + * Use the default SSSS key if the default is set + [\#5638](https://github.com/matrix-org/matrix-react-sdk/pull/5638) + * Initial Spaces feature flag + [\#5668](https://github.com/matrix-org/matrix-react-sdk/pull/5668) + * Clean up code edge cases and add helpers + [\#5667](https://github.com/matrix-org/matrix-react-sdk/pull/5667) + * Clean up widgets when leaving the room + [\#5684](https://github.com/matrix-org/matrix-react-sdk/pull/5684) + * Fix read receipts? + [\#5567](https://github.com/matrix-org/matrix-react-sdk/pull/5567) + * Fix MAU usage alerts + [\#5678](https://github.com/matrix-org/matrix-react-sdk/pull/5678) + Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0) diff --git a/docs/media-handling.md b/docs/media-handling.md new file mode 100644 index 0000000000..a4307fb7d4 --- /dev/null +++ b/docs/media-handling.md @@ -0,0 +1,19 @@ +# Media handling + +Surely media should be as easy as just putting a URL into an `img` and calling it good, right? +Not quite. Matrix uses something called a Matrix Content URI (better known as MXC URI) to identify +content, which is then converted to a regular HTTPS URL on the homeserver. However, sometimes that +URL can change depending on deployment considerations. + +The react-sdk features a [customisation endpoint](https://github.com/vector-im/element-web/blob/develop/docs/customisations.md) +for media handling where all conversions from MXC URI to HTTPS URL happen. This is to ensure that +those obscure deployments can route all their media to the right place. + +For development, there are currently two functions available: `mediaFromMxc` and `mediaFromContent`. +The `mediaFromMxc` function should be self-explanatory. `mediaFromContent` takes an event content as +a parameter and will automatically parse out the source media and thumbnail. Both functions return +a `Media` object with a number of options on it, such as getting various common HTTPS URLs for the +media. + +**It is extremely important that all media calls are put through this customisation endpoint.** So +much so it's a lint rule to avoid accidental use of the wrong functions. diff --git a/package.json b/package.json index 7ed1b272da..f8b4287197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.15.0", + "version": "3.16.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -157,6 +157,7 @@ "jest": "^26.6.3", "jest-canvas-mock": "^2.3.0", "jest-environment-jsdom-sixteen": "^1.0.3", + "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index f1f27014ee..10357117da 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -19,7 +19,8 @@ $roomListCollapsedWidth: 68px; .mx_LeftPanel { background-color: $roomlist-bg-color; - min-width: 260px; + // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel + min-width: 206px; max-width: 50%; // Create a row-based flexbox for the GroupFilterPanel and the room list diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 812a7f8472..a220c5d505 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -66,7 +66,7 @@ limitations under the License. } /* not the left panel, and not the resize handle, so the roomview/groupview/... */ -.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_ResizeHandle) { +.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_SpacePanel):not(.mx_ResizeHandle) { background-color: $primary-bg-color; flex: 1 1 0; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 9937117086..d3e7d7efee 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -16,9 +16,8 @@ limitations under the License. $topLevelHeight: 32px; $nestedHeight: 24px; -$gutterSize: 17px; -$activeStripeSize: 4px; -$activeBorderTransparentGap: 2px; +$gutterSize: 16px; +$activeBorderTransparentGap: 1px; $activeBackgroundColor: $roomtile-selected-bg-color; $activeBorderColor: $secondary-fg-color; @@ -36,6 +35,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_spaceTreeWrapper { flex: 1; + overflow-y: scroll; } .mx_SpacePanel_toggleCollapse { @@ -63,21 +63,26 @@ $activeBorderColor: $secondary-fg-color; } .mx_AutoHideScrollbar { - padding: 16px 12px 16px 0; + padding: 16px 0; } .mx_SpaceButton_toggleCollapse { cursor: pointer; } - .mx_SpaceItem.collapsed { - .mx_SpaceButton { - .mx_NotificationBadge { - right: -4px; - top: -4px; - } - } + .mx_SpaceTreeLevel { + display: flex; + flex-direction: column; + max-width: 250px; + flex-grow: 1; + } + .mx_SpaceItem { + display: inline-flex; + flex-flow: wrap; + } + + .mx_SpaceItem.collapsed { & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse { transform: rotate(-90deg); } @@ -89,34 +94,43 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { margin-left: $gutterSize; + min-width: 40px; } .mx_SpaceButton { border-radius: 8px; - position: relative; margin-bottom: 2px; display: flex; align-items: center; - padding: 4px; + padding: 4px 4px 4px 0; + width: 100%; &.mx_SpaceButton_active { &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper { background-color: $activeBackgroundColor; - border-radius: 8px; } - &.mx_SpaceButton_narrow { - .mx_BaseAvatar, .mx_SpaceButton_avatarPlaceholder { - border: 2px $activeBorderColor solid; - border-radius: 11px; - } + &.mx_SpaceButton_narrow .mx_SpaceButton_selectionWrapper { + padding: $activeBorderTransparentGap; + border: 3px $activeBorderColor solid; } } .mx_SpaceButton_selectionWrapper { + position: relative; display: flex; flex: 1; align-items: center; + border-radius: 12px; + padding: 4px; + } + + &:not(.mx_SpaceButton_narrow) { + .mx_SpaceButton_selectionWrapper { + width: 100%; + padding-right: 16px; + overflow: hidden; + } } .mx_SpaceButton_name { @@ -124,7 +138,6 @@ $activeBorderColor: $secondary-fg-color; margin-left: 8px; white-space: nowrap; display: block; - max-width: 150px; text-overflow: ellipsis; overflow: hidden; padding-right: 8px; @@ -133,8 +146,10 @@ $activeBorderColor: $secondary-fg-color; } .mx_SpaceButton_toggleCollapse { - width: calc($gutterSize - $activeStripeSize); - margin-left: 1px; + width: $gutterSize; + // negative margin to place it correctly even with the complex + // 4px selection border each space button has when active + margin-right: -4px; height: 20px; mask-position: center; mask-size: 20px; @@ -172,11 +187,6 @@ $activeBorderColor: $secondary-fg-color; } } - .mx_SpaceButton_avatarPlaceholder { - border: $activeBorderTransparentGap transparent solid; - padding: $activeBorderTransparentGap; - } - &.mx_SpaceButton_new .mx_SpaceButton_icon { background-color: $accent-color; transition: all .1s ease-in-out; // TODO transition @@ -196,21 +206,8 @@ $activeBorderColor: $secondary-fg-color; } } - .mx_BaseAvatar { - /* moving the border-radius to this element from _image - element so we can add a border to it without the initials being displaced */ - overflow: hidden; - border: 2px transparent solid; - padding: $activeBorderTransparentGap; - - .mx_BaseAvatar_initial { - top: $activeBorderTransparentGap; - left: $activeBorderTransparentGap; - } - - .mx_BaseAvatar_image { - border-radius: 8px; - } + .mx_BaseAvatar_image { + border-radius: 8px; } .mx_SpaceButton_menuButton { @@ -219,8 +216,9 @@ $activeBorderColor: $secondary-fg-color; height: 20px; margin-top: auto; margin-bottom: auto; - position: relative; display: none; + position: absolute; + right: 4px; &::before { top: 2px; @@ -239,9 +237,8 @@ $activeBorderColor: $secondary-fg-color; } .mx_SpacePanel_badgeContainer { + position: absolute; height: 16px; - // don't set width so that it takes no space when there is no badge to show - margin: auto 0; // vertically align // Create a flexbox to make aligning dot badges easier display: flex; @@ -261,14 +258,25 @@ $activeBorderColor: $secondary-fg-color; &.collapsed { .mx_SpaceButton { .mx_SpacePanel_badgeContainer { - position: absolute; - right: 0px; - top: 2px; + right: -3px; + top: -3px; + } + + &.mx_SpaceButton_active .mx_SpacePanel_badgeContainer { + // when we draw the selection border we move the relative bounds of our parent + // so update our position within the bounds of the parent to maintain position overall + right: -6px; + top: -6px; } } } &:not(.collapsed) { + .mx_SpacePanel_badgeContainer { + position: absolute; + right: 4px; + } + .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index c96398594f..b14e92a1af 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -132,6 +132,7 @@ limitations under the License. height: min-content; margin-left: auto; margin-right: 16px; + display: inline-flex; } } diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index f742be70e4..80e7aaada0 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -26,50 +26,6 @@ limitations under the License. position: relative; } -.mx_CompleteSecurity_clients { - width: max-content; - margin: 36px auto 0; - - .mx_CompleteSecurity_clients_desktop, .mx_CompleteSecurity_clients_mobile { - position: relative; - width: 160px; - text-align: center; - padding-top: 64px; - display: inline-block; - - &::before { - content: ''; - position: absolute; - height: 48px; - width: 48px; - left: 56px; - top: 0; - background-color: $muted-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - } - } - - .mx_CompleteSecurity_clients_desktop { - margin-right: 56px; - } - - .mx_CompleteSecurity_clients_desktop::before { - mask-image: url('$(res)/img/feather-customised/monitor.svg'); - } - - .mx_CompleteSecurity_clients_mobile::before { - mask-image: url('$(res)/img/feather-customised/smartphone.svg'); - } - - p { - margin-top: 16px; - font-size: $font-12px; - color: $muted-fg-color; - text-align: center; - } -} - .mx_CompleteSecurity_heroIcon { width: 128px; height: 128px; diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index e219c0c5e4..b45126acf8 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015, 2016, 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. @@ -16,6 +16,19 @@ limitations under the License. .mx_MFileBody_download { color: $accent-color; + + .mx_MFileBody_download_icon { + // 12px instead of 14px to better match surrounding font size + width: 12px; + height: 12px; + mask-size: 12px; + + mask-position: center; + mask-repeat: no-repeat; + mask-image: url("$(res)/img/download.svg"); + background-color: $accent-color; + display: inline-block; + } } .mx_MFileBody_download a { diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 8eca3f1efa..72d29dfd4c 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -189,6 +189,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/settings.svg'); } + .mx_RoomTile_iconInvite::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + .mx_RoomTile_iconSignOut::before { mask-image: url('$(res)/img/element-icons/leave.svg'); } diff --git a/res/img/feather-customised/monitor.svg b/res/img/feather-customised/monitor.svg deleted file mode 100644 index 231811d5a6..0000000000 --- a/res/img/feather-customised/monitor.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/smartphone.svg b/res/img/feather-customised/smartphone.svg deleted file mode 100644 index fde78c82e2..0000000000 --- a/res/img/feather-customised/smartphone.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/Avatar.ts b/src/Avatar.ts index e2557e21a8..76c88faa1c 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -14,27 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import {User} from "matrix-js-sdk/src/models/user"; import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; +import {mediaFromMxc} from "./customisations/Media"; export type ResizeMethod = "crop" | "scale"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { let url: string; - if (member && member.getAvatarUrl) { - url = member.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), + if (member?.getMxcAvatarUrl()) { + url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( Math.floor(width * window.devicePixelRatio), Math.floor(height * window.devicePixelRatio), resizeMethod, - false, - false, ); } if (!url) { @@ -47,16 +43,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu } export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { - const url = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, + if (!user.avatarUrl) return null; + return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp( Math.floor(width * window.devicePixelRatio), Math.floor(height * window.devicePixelRatio), resizeMethod, ); - if (!url || url.length === 0) { - return null; - } - return url; } function isValidHexColor(color: string): boolean { @@ -154,15 +146,8 @@ export function getInitialLetter(name: string): string { export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) { if (!room) return null; // null-guard - const explicitRoomAvatar = room.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - width, - height, - resizeMethod, - false, - ); - if (explicitRoomAvatar) { - return explicitRoomAvatar; + if (room.getMxcAvatarUrl()) { + return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } // space rooms cannot be DMs so skip the rest @@ -177,14 +162,8 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi // then still try to show any avatar (pref. other member) otherMember = room.getAvatarFallbackMember(); } - if (otherMember) { - return otherMember.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - width, - height, - resizeMethod, - false, - ); + if (otherMember?.getMxcAvatarUrl()) { + return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } return null; } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index d2564e637b..ce779f12a5 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -788,6 +788,11 @@ export default class CallHandler { // don't remove the call yet: let the hangup event handler do it (otherwise it will throw // the hangup event away) break; + case 'hangup_all': + for (const call of this.calls.values()) { + call.hangup(CallErrorCode.UserHangup, false); + } + break; case 'answer': { if (!this.calls.has(payload.room_id)) { return; // no call to answer diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index 8d56467c57..7c7940cab5 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -14,9 +14,9 @@ limitations under the License. */ -import * as Matrix from 'matrix-js-sdk'; import SettingsStore from "./settings/SettingsStore"; import {SettingLevel} from "./settings/SettingLevel"; +import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix"; export default { hasAnyLabeledDevices: async function() { @@ -54,24 +54,24 @@ export default { const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); - Matrix.setMatrixCallAudioOutput(audioOutDeviceId); - Matrix.setMatrixCallAudioInput(audioDeviceId); - Matrix.setMatrixCallVideoInput(videoDeviceId); + setMatrixCallAudioOutput(audioOutDeviceId); + setMatrixCallAudioInput(audioDeviceId); + setMatrixCallVideoInput(videoDeviceId); }, setAudioOutput: function(deviceId) { SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); - Matrix.setMatrixCallAudioOutput(deviceId); + setMatrixCallAudioOutput(deviceId); }, setAudioInput: function(deviceId) { SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); - Matrix.setMatrixCallAudioInput(deviceId); + setMatrixCallAudioInput(deviceId); }, setVideoInput: function(deviceId) { SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); - Matrix.setMatrixCallVideoInput(deviceId); + setMatrixCallVideoInput(deviceId); }, getAudioOutput: function() { diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 7d6b049914..1dc342fac5 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -32,10 +32,10 @@ import { AllHtmlEntities } from 'html-entities'; import SettingsStore from './settings/SettingsStore'; import cheerio from 'cheerio'; -import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; +import {mediaFromMxc} from "./customisations/Media"; linkifyMatrix(linkify); @@ -181,11 +181,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) { return { tagName, attribs: {}}; } - attribs.src = MatrixClientPeg.get().mxcUrlToHttp( - attribs.src, - attribs.width || 800, - attribs.height || 600, - ); + const width = Number(attribs.width) || 800; + const height = Number(attribs.height) || 600; + attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height); return { tagName, attribs }; }, 'code': function(tagName: string, attribs: sanitizeHtml.Attributes) { @@ -239,6 +237,7 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = { 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', + 'details', 'summary', ], allowedAttributes: { // custom ones first: diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index d3bfee2380..1687adf13b 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createClient, SERVICE_TYPES } from 'matrix-js-sdk'; +import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types'; +import { createClient } from 'matrix-js-sdk/src/matrix'; import {MatrixClientPeg} from './MatrixClientPeg'; import Modal from './Modal'; diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 7780d4c87a..b0a1292ba1 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -17,8 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising -import Matrix from 'matrix-js-sdk'; +import { createClient } from 'matrix-js-sdk/src/matrix'; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { MatrixClient } from "matrix-js-sdk/src/client"; import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes"; @@ -219,7 +218,7 @@ export function attemptTokenLogin( button: _t("Try again"), onFinished: tryAgain => { if (tryAgain) { - const cli = Matrix.createClient({ + const cli = createClient({ baseUrl: homeserver, idBaseUrl: identityServer, }); @@ -276,7 +275,7 @@ function registerAsGuest( console.log(`Doing guest login on ${hsUrl}`); // create a temporary MatrixClient to do the login - const client = Matrix.createClient({ + const client = createClient({ baseUrl: hsUrl, }); diff --git a/src/Login.ts b/src/Login.ts index aecc0493c7..db3c4c11e4 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -19,7 +19,7 @@ limitations under the License. */ // @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising -import Matrix from "matrix-js-sdk"; +import {createClient} from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { IMatrixClientCreds } from "./MatrixClientPeg"; import SecurityCustomisations from "./customisations/Security"; @@ -115,7 +115,7 @@ export default class Login { */ public createTemporaryClient(): MatrixClient { if (this.tempClient) return this.tempClient; // use memoization - return this.tempClient = Matrix.createClient({ + return this.tempClient = createClient({ baseUrl: this.hsUrl, idBaseUrl: this.isUrl, }); @@ -210,7 +210,7 @@ export async function sendLoginRequest( loginType: string, loginParams: ILoginParams, ): Promise { - const client = Matrix.createClient({ + const client = createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, }); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 98ca446532..de1d573d40 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -261,7 +261,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { } public getHomeserverName(): string { - const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId); + const matches = /^@[^:]+:(.+)$/.exec(this.matrixClient.credentials.userId); if (matches === null || matches.length < 1) { throw new Error("Failed to derive homeserver name from user ID!"); } diff --git a/src/Notifier.ts b/src/Notifier.ts index 6460be20ad..f68bfabc18 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel"; import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers"; import RoomViewStore from "./stores/RoomViewStore"; import UserActivity from "./UserActivity"; +import {mediaFromMxc} from "./customisations/Media"; /* * Dispatches: @@ -150,7 +151,7 @@ export const Notifier = { // Ideally in here we could use MSC1310 to detect the type of file, and reject it. return { - url: MatrixClientPeg.get().mxcUrlToHttp(content.url), + url: mediaFromMxc(content.url).srcHttp, name: content.name, type: content.type, size: content.size, diff --git a/src/PasswordReset.js b/src/PasswordReset.js index b38a9de960..6fe6ca82cc 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Matrix from 'matrix-js-sdk'; +import { createClient } from 'matrix-js-sdk/src/matrix'; import { _t } from './languageHandler'; /** @@ -32,7 +32,7 @@ export default class PasswordReset { * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping. */ constructor(homeserverUrl, identityUrl) { - this.client = Matrix.createClient({ + this.client = createClient({ baseUrl: homeserverUrl, idBaseUrl: identityUrl, }); diff --git a/src/Resend.js b/src/Resend.js index 5638313306..bf69e59c1a 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -17,7 +17,7 @@ limitations under the License. import {MatrixClientPeg} from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; -import { EventStatus } from 'matrix-js-sdk'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; export default class Resend { static resendUnsentEvents(room) { diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 1ea9d39e2f..200b4fd7b9 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -21,9 +21,9 @@ import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; import {MatrixClientPeg} from "./MatrixClientPeg"; import request from "browser-request"; -import * as Matrix from 'matrix-js-sdk'; import SdkConfig from "./SdkConfig"; import {WidgetType} from "./widgets/WidgetType"; +import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; @@ -153,7 +153,7 @@ export default class ScalarAuthClient { parsedImRestUrl.path = ''; parsedImRestUrl.pathname = ''; return startTermsFlow([new Service( - Matrix.SERVICE_TYPES.IM, + SERVICE_TYPES.IM, parsedImRestUrl.format(), token, )], this.termsInteractionCallback).then(() => { diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 896e27d92c..3f75b3788c 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -237,7 +237,7 @@ Example: */ import {MatrixClientPeg} from './MatrixClientPeg'; -import { MatrixEvent } from 'matrix-js-sdk'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import RoomViewStore from './stores/RoomViewStore'; diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index 4dd296a8f1..eeb68b94bd 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -19,7 +19,7 @@ import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; -import { MatrixClient } from 'matrix-js-sdk'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as sdk from '../../../../index'; diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js index e7bae3578b..670cb28b94 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js @@ -17,7 +17,7 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import { MatrixClient } from 'matrix-js-sdk'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as sdk from '../../../../index'; import { _t } from '../../../../languageHandler'; diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index ebf5d536ec..b7a4e0960e 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -27,6 +27,7 @@ import {sortBy} from "lodash"; import {makeGroupPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; +import {mediaFromMxc} from "../customisations/Media"; const COMMUNITY_REGEX = /\B\+\S*/g; @@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider { name={name || groupId} width={24} height={24} - url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} /> + url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} /> ), range, diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 9f5a0b6211..32db5c251c 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {Filter} from 'matrix-js-sdk'; +import {Filter} from 'matrix-js-sdk/src/filter'; import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import EventIndexPeg from "../../indexing/EventIndexPeg"; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index b4b871a0b4..b006b323fb 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -35,10 +35,11 @@ import GroupStore from '../../stores/GroupStore'; import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; -import {Group} from "matrix-js-sdk"; +import {Group} from "matrix-js-sdk/src/models/group"; import {allSettled, sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; +import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( @@ -368,8 +369,7 @@ class FeaturedUser extends React.Component { const permalink = makeUserPermalink(this.props.summaryInfo.user_id); const userNameNode = { name }; - const httpUrl = MatrixClientPeg.get() - .mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64); + const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64); const deleteButton = this.props.editing ? ; } - const httpInviterAvatar = this.state.inviterProfile ? - this._matrixClient.mxcUrlToHttp( - this.state.inviterProfile.avatarUrl, 36, 36, - ) : null; + const httpInviterAvatar = this.state.inviterProfile + ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36) + : null; const inviter = group.inviter || {}; let inviterName = inviter.userId; diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 9b61f71fd7..d419c9de6e 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InteractiveAuth} from "matrix-js-sdk"; +import {InteractiveAuth} from "matrix-js-sdk/src/interactive-auth"; import React, {createRef} from 'react'; import PropTypes from 'prop-types'; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 88c7a71b35..f46319235a 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -36,11 +36,10 @@ import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; -import SpacePanel from "../views/spaces/SpacePanel"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../customisations/Media"; interface IProps { isMinimized: boolean; @@ -121,7 +120,7 @@ export default class LeftPanel extends React.Component { let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage"); if (settingBgMxc) { - avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize); + avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize); } const avatarUrlProp = `url(${avatarUrl})`; @@ -392,11 +391,7 @@ export default class LeftPanel extends React.Component { public render(): React.ReactNode { let leftLeftPanel; - // Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now - // ignore it and force the rendering of SpacePanel if that Labs flag is enabled. - if (SettingsStore.getValue("feature_spaces")) { - leftLeftPanel = ; - } else if (this.state.showGroupFilterPanel) { + if (this.state.showGroupFilterPanel) { leftLeftPanel = (
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 15e90a383a..60a2bf4ada 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -56,6 +56,7 @@ import Modal from "../../Modal"; import { ICollapseConfig } from "../../resizer/distributors/collapse"; import HostSignupContainer from '../views/host_signup/HostSignupContainer'; import { IOpts } from "../../createRoom"; +import SpacePanel from "../views/spaces/SpacePanel"; import {replaceableComponent} from "../../utils/replaceableComponent"; // We need to fetch each pinned message individually (if we don't already have it) @@ -229,21 +230,15 @@ class LoggedInView extends React.Component { let size; let collapsed; const collapseConfig: ICollapseConfig = { - // TODO: the space panel currently does not have a fixed width, - // just the headers at each level have a max-width of 150px - // Taking 222px for the space panel for now, - // so this will look slightly off for now, - // depending on the depth of your space tree. - // To fix this, we'll need to turn toggleSize - // into a callback so it can be measured when starting the resize operation - toggleSize: 222 + 68, + // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel + toggleSize: 206 - 50, onCollapsed: (_collapsed) => { collapsed = _collapsed; if (_collapsed) { - dis.dispatch({action: "hide_left_panel"}, true); + dis.dispatch({action: "hide_left_panel"}); window.localStorage.setItem("mx_lhs_size", '0'); } else { - dis.dispatch({action: "show_left_panel"}, true); + dis.dispatch({action: "show_left_panel"}); } }, onResized: (_size) => { @@ -670,13 +665,6 @@ class LoggedInView extends React.Component { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } - const leftPanel = ( - - ); - return (
{
- { leftPanel } + { SettingsStore.getValue("feature_spaces") ? : null } + { pageElement }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0272633e8f..ac1842c7fd 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1,8 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2017-2019 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,8 +15,7 @@ limitations under the License. */ import React, { createRef } from 'react'; -// @ts-ignore - XXX: no idea why this import fails -import * as Matrix from "matrix-js-sdk"; +import { createClient } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -82,9 +78,12 @@ import {UIFeature} from "../../settings/UIFeature"; import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import DialPadModal from "../views/voip/DialPadModal"; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; +import { shouldUseLoginForWelcome } from "../../utils/pages"; import SpaceStore from "../../stores/SpaceStore"; import SpaceRoomDirectory from "./SpaceRoomDirectory"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import RoomListStore from "../../stores/room-list/RoomListStore"; +import {RoomUpdateCause} from "../../stores/room-list/models"; /** constants for MatrixChat.state.view */ export enum Views { @@ -582,6 +581,7 @@ export default class MatrixChat extends React.PureComponent { } break; case 'logout': + dis.dispatch({action: "hangup_all"}); Lifecycle.logout(); break; case 'require_registration': @@ -606,12 +606,7 @@ export default class MatrixChat extends React.PureComponent { if (payload.screenAfterLogin) { this.screenAfterLogin = payload.screenAfterLogin; } - this.setStateForNewView({ - view: Views.LOGIN, - }); - this.notifyNewScreen('login'); - ThemeController.isLogin = true; - this.themeWatcher.recheck(); + this.viewLogin(); break; case 'start_password_recovery': this.setStateForNewView({ @@ -975,6 +970,9 @@ export default class MatrixChat extends React.PureComponent { } private viewWelcome() { + if (shouldUseLoginForWelcome(SdkConfig.get())) { + return this.viewLogin(); + } this.setStateForNewView({ view: Views.WELCOME, }); @@ -983,6 +981,16 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } + private viewLogin(otherState?: any) { + this.setStateForNewView({ + view: Views.LOGIN, + ...otherState, + }); + this.notifyNewScreen('login'); + ThemeController.isLogin = true; + this.themeWatcher.recheck(); + } + private viewHome(justRegistered = false) { // The home page requires the "logged in" view, so we'll set that. this.setStateForNewView({ @@ -1139,11 +1147,17 @@ export default class MatrixChat extends React.PureComponent { } private forgetRoom(roomId: string) { + const room = MatrixClientPeg.get().getRoom(roomId); MatrixClientPeg.get().forget(roomId).then(() => { // Switch to home page if we're currently viewing the forgotten room if (this.state.currentRoomId === roomId) { dis.dispatch({ action: "view_home_page" }); } + + // We have to manually update the room list because the forgotten room will not + // be notified to us, therefore the room list will have no other way of knowing + // the room is forgotten. + RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved); }).catch((err) => { const errCode = err.errcode || _td("unknown error code"); Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, { @@ -1298,17 +1312,13 @@ export default class MatrixChat extends React.PureComponent { * Called when the session is logged out */ private onLoggedOut() { - this.notifyNewScreen('login'); - this.setStateForNewView({ - view: Views.LOGIN, + this.viewLogin({ ready: false, collapseLhs: false, currentRoomId: null, }); this.subTitleStatus = ''; this.setPageSubtitle(); - ThemeController.isLogin = true; - this.themeWatcher.recheck(); } /** @@ -1648,7 +1658,7 @@ export default class MatrixChat extends React.PureComponent { let cli = MatrixClientPeg.get(); if (!cli) { const {hsUrl, isUrl} = this.props.serverConfig; - cli = Matrix.createClient({ + cli = createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, }); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 439bdbcef5..63f23f22f3 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -23,7 +23,6 @@ import classNames from 'classnames'; import shouldHideEvent from '../../shouldHideEvent'; import {wantsDateSeparator} from '../../DateUtils'; import * as sdk from '../../index'; -import dis from "../../dispatcher/dispatcher"; import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; @@ -210,13 +209,11 @@ export default class MessagePanel extends React.Component { componentDidMount() { this._isMounted = true; - this.dispatcherRef = dis.register(this.onAction); } componentWillUnmount() { this._isMounted = false; SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); - dis.unregister(this.dispatcherRef); } componentDidUpdate(prevProps, prevState) { @@ -229,14 +226,6 @@ export default class MessagePanel extends React.Component { } } - onAction = (payload) => { - switch (payload.action) { - case "scroll_to_bottom": - this.scrollToBottom(); - break; - } - } - onShowTypingNotificationsChange = () => { this.setState({ showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), @@ -1038,6 +1027,97 @@ class CreationGrouper { } } +class RedactionGrouper { + static canStartGroup = function(panel, ev) { + return panel._shouldShowEvent(ev) && ev.isRedacted(); + } + + constructor(panel, ev, prevEvent, lastShownEvent) { + this.panel = panel; + this.readMarker = panel._readMarkerForEvent( + ev.getId(), + ev === lastShownEvent, + ); + this.events = [ev]; + this.prevEvent = prevEvent; + this.lastShownEvent = lastShownEvent; + } + + shouldGroup(ev) { + if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { + return false; + } + return ev.isRedacted(); + } + + add(ev) { + this.readMarker = this.readMarker || this.panel._readMarkerForEvent( + ev.getId(), + ev === this.lastShownEvent, + ); + this.events.push(ev); + } + + getTiles() { + if (!this.events || !this.events.length) return []; + + const DateSeparator = sdk.getComponent('messages.DateSeparator'); + const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); + + const panel = this.panel; + const ret = []; + const lastShownEvent = this.lastShownEvent; + + if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { + const ts = this.events[0].getTs(); + ret.push( +
  • , + ); + } + + const key = "redactioneventlistsummary-" + ( + this.prevEvent ? this.events[0].getId() : "initial" + ); + + const senders = new Set(); + let eventTiles = this.events.map((e) => { + senders.add(e.sender); + // In order to prevent DateSeparators from appearing in the expanded form, + // render each member event as if the previous one was itself. + // This way, the timestamp of the previous event === the + // timestamp of the current event, and no DateSeparator is inserted. + return panel._getTilesForEvent(e, e, e === lastShownEvent); + }).reduce((a, b) => a.concat(b), []); + + if (eventTiles.length === 0) { + eventTiles = null; + } + + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + getNewPrevEvent() { + return this.events[0]; + } +} + // Wrap consecutive member events in a ListSummary, ignore if redacted class MemberGrouper { static canStartGroup = function(panel, ev) { @@ -1148,4 +1228,4 @@ class MemberGrouper { } // all the grouper classes that we use -const groupers = [CreationGrouper, MemberGrouper]; +const groupers = [CreationGrouper, MemberGrouper, RedactionGrouper]; diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 363c67262b..3613261da6 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -27,7 +27,6 @@ import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; import SettingsStore from "../../settings/SettingsStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore"; @@ -35,6 +34,7 @@ import GroupStore from "../../stores/GroupStore"; import FlairStore from "../../stores/FlairStore"; import CountlyAnalytics from "../../CountlyAnalytics"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../customisations/Media"; const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 800; @@ -521,10 +521,9 @@ export default class RoomDirectory extends React.Component { topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; } topic = linkifyAndSanitizeHtml(topic); - const avatarUrl = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - room.avatar_url, 32, 32, "crop", - ); + let avatarUrl = null; + if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32); + return [
    this.onRoomClicked(room, ev)} diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 8b70998be0..54b6fee233 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import Matrix from 'matrix-js-sdk'; import { _t, _td } from '../../languageHandler'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; @@ -24,6 +23,7 @@ import dis from '../../dispatcher/dispatcher'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {Action} from "../../dispatcher/actions"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {EventStatus} from "matrix-js-sdk/src/models/event"; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -32,7 +32,7 @@ const STATUS_BAR_EXPANDED_LARGE = 2; function getUnsentMessages(room) { if (!room) { return []; } return room.getPendingEvents().filter(function(ev) { - return ev.status === Matrix.EventStatus.NOT_SENT; + return ev.status === EventStatus.NOT_SENT; }); } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 72e52678b6..9ee16558d3 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -34,6 +34,7 @@ import {EnhancedMap} from "../../utils/maps"; import StyledCheckbox from "../views/elements/StyledCheckbox"; import AutoHideScrollbar from "./AutoHideScrollbar"; import BaseAvatar from "../views/avatars/BaseAvatar"; +import {mediaFromMxc} from "../../customisations/Media"; interface IProps { space: Room; @@ -158,12 +159,7 @@ const SubSpace: React.FC = ({ let url: string; if (space.avatar_url) { - url = MatrixClientPeg.get().mxcUrlToHttp( - space.avatar_url, - Math.floor(24 * window.devicePixelRatio), - Math.floor(24 * window.devicePixelRatio), - "crop", - ); + url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio)); } return
    @@ -265,12 +261,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli let url: string; if (room.avatar_url) { - url = cli.mxcUrlToHttp( - room.avatar_url, - Math.floor(32 * window.devicePixelRatio), - Math.floor(32 * window.devicePixelRatio), - "crop", - ); + url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio)); } const content = diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index f32b8ed0a9..12f5d6e890 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -22,8 +22,8 @@ import {LayoutPropType} from "../../settings/Layout"; import React, {createRef} from 'react'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; -import {EventTimeline} from "matrix-js-sdk"; -import * as Matrix from "matrix-js-sdk"; +import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; +import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; import { _t } from '../../languageHandler'; import {MatrixClientPeg} from "../../MatrixClientPeg"; import UserActivity from "../../UserActivity"; @@ -463,6 +463,9 @@ class TimelinePanel extends React.Component { } }); } + if (payload.action === "scroll_to_bottom") { + this.jumpToLiveTimeline(); + } }; onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { @@ -1007,7 +1010,7 @@ class TimelinePanel extends React.Component { * returns a promise which will resolve when the load completes. */ _loadTimeline(eventId, pixelOffset, offsetBase) { - this._timelineWindow = new Matrix.TimelineWindow( + this._timelineWindow = new TimelineWindow( MatrixClientPeg.get(), this.props.timelineSet, {windowLimit: this.props.timelineCap}); diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js index dc05193ece..6b472783bb 100644 --- a/src/components/structures/UserView.js +++ b/src/components/structures/UserView.js @@ -17,13 +17,14 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; -import Matrix from "matrix-js-sdk"; import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as sdk from "../../index"; import Modal from '../../Modal'; import { _t } from '../../languageHandler'; import HomePage from "./HomePage"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; @replaceableComponent("structures.UserView") export default class UserView extends React.Component { @@ -68,8 +69,8 @@ export default class UserView extends React.Component { this.setState({loading: false}); return; } - const fakeEvent = new Matrix.MatrixEvent({type: "m.room.member", content: profileInfo}); - const member = new Matrix.RoomMember(null, this.props.userId); + const fakeEvent = new MatrixEvent({type: "m.room.member", content: profileInfo}); + const member = new RoomMember(null, this.props.userId); member.setMembershipEvent(fakeEvent); this.setState({member, loading: false}); } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 32bdddb82a..9d004de2ec 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Matrix from 'matrix-js-sdk'; +import {createClient} from 'matrix-js-sdk/src/matrix'; import React, {ReactNode} from 'react'; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -181,7 +181,7 @@ export default class Registration extends React.Component { } const {hsUrl, isUrl} = serverConfig; - const cli = Matrix.createClient({ + const cli = createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, }); diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 799a559263..e623439174 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {toPx} from "../../../utils/units"; +import {ResizeMethod} from "../../../Avatar"; interface IProps { name: string; // The name (first initial used as default) @@ -35,7 +36,7 @@ interface IProps { width?: number; height?: number; // XXX: resizeMethod not actually used. - resizeMethod?: string; + resizeMethod?: ResizeMethod; defaultToInitialLetter?: boolean; // true to add default url onClick?: React.MouseEventHandler; inputRef?: React.RefObject; diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index a033257871..3734ba9504 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd +Copyright 2017, 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. @@ -15,9 +15,10 @@ limitations under the License. */ import React from 'react'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; import BaseAvatar from './BaseAvatar'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; +import {ResizeMethod} from "../../../Avatar"; export interface IProps { groupId?: string; @@ -25,7 +26,7 @@ export interface IProps { groupAvatarUrl?: string; width?: number; height?: number; - resizeMethod?: string; + resizeMethod?: ResizeMethod; onClick?: React.MouseEventHandler; } @@ -38,8 +39,8 @@ export default class GroupAvatar extends React.Component { }; getGroupAvatarUrl() { - return MatrixClientPeg.get().mxcUrlToHttp( - this.props.groupAvatarUrl, + if (!this.props.groupAvatarUrl) return null; + return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp( this.props.width, this.props.height, this.props.resizeMethod, diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 641046aa55..c79cbc0d32 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -20,16 +20,17 @@ import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import dis from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; import BaseAvatar from "./BaseAvatar"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; +import {ResizeMethod} from "../../../Avatar"; interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember; fallbackUserId?: string; width: number; height: number; - resizeMethod?: string; + resizeMethod?: ResizeMethod; // The onClick to give the avatar onClick?: React.MouseEventHandler; // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` @@ -63,18 +64,19 @@ export default class MemberAvatar extends React.Component { } private static getState(props: IProps): IState { - if (props.member && props.member.name) { - return { - name: props.member.name, - title: props.title || props.member.userId, - imageUrl: props.member.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), + if (props.member?.name) { + let imageUrl = null; + if (props.member.getMxcAvatarUrl()) { + imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( Math.floor(props.width * window.devicePixelRatio), Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, - false, - false, - ), + ); + } + return { + name: props.member.name, + title: props.title || props.member.userId, + imageUrl: imageUrl, }; } else if (props.fallbackUserId) { return { diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 0a59f6e36a..31245b44b7 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React, {ComponentProps} from 'react'; import Room from 'matrix-js-sdk/src/models/room'; -import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; @@ -24,6 +23,7 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import {ResizeMethod} from "../../../Avatar"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, @@ -90,16 +90,16 @@ export default class RoomAvatar extends React.Component { }; private static getImageUrls(props: IProps): string[] { - return [ - getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - // Default props don't play nicely with getDerivedStateFromProps - //props.oobData !== undefined ? props.oobData.avatarUrl : {}, - props.oobData.avatarUrl, + let oobAvatar = null; + if (props.oobData.avatarUrl) { + oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp( Math.floor(props.width * window.devicePixelRatio), Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, - ), // highest priority + ); + } + return [ + oobAvatar, // highest priority RoomAvatar.getRoomAvatarUrl(props), ].filter(function(url) { return (url !== null && url !== ""); diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx index 04cfce7670..cca158269e 100644 --- a/src/components/views/avatars/WidgetAvatar.tsx +++ b/src/components/views/avatars/WidgetAvatar.tsx @@ -14,21 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ComponentProps, useContext} from 'react'; +import React, {ComponentProps} from 'react'; import classNames from 'classnames'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {IApp} from "../../../stores/WidgetStore"; import BaseAvatar, {BaseAvatarType} from "./BaseAvatar"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends Omit, "name" | "url" | "urls"> { app: IApp; } const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 20, ...props }) => { - const cli = useContext(MatrixClientContext); - let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")]; // heuristics for some better icons until Widgets support their own icons if (app.type.includes("jitsi")) { @@ -47,7 +44,7 @@ const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 2 name={app.id} className={classNames("mx_WidgetAvatar", className)} // MSC2765 - url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined} + url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined} urls={iconUrls} width={width} height={height} diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js index 11a9d90ac2..15078326b3 100644 --- a/src/components/views/context_menus/GroupInviteTileContextMenu.js +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; -import {Group} from 'matrix-js-sdk'; +import {Group} from 'matrix-js-sdk/src/models/group'; import GroupStore from "../../../stores/GroupStore"; import {MenuItem} from "../../structures/ContextMenu"; import {replaceableComponent} from "../../../utils/replaceableComponent"; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index daabf224dc..56f070ba36 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -19,7 +19,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {EventStatus} from 'matrix-js-sdk'; +import {EventStatus} from 'matrix-js-sdk/src/models/event'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index d1080566ac..2635f95bb7 100644 --- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -26,12 +26,12 @@ import SdkConfig from "../../../SdkConfig"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import InviteDialog from "./InviteDialog"; import BaseAvatar from "../avatars/BaseAvatar"; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite"; import StyledCheckbox from "../elements/StyledCheckbox"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends IDialogProps { roomId: string; @@ -142,12 +142,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< private renderPerson(person: IPerson, key: any) { const avatarSize = 36; + let avatarUrl = null; + if (person.user.getMxcAvatarUrl()) { + avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize); + } return (
    ; diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index dfcc5d6dfb..9f5513e0a3 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -19,7 +19,6 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; -import { Room, MatrixEvent } from "matrix-js-sdk"; import Field from "../elements/Field"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; @@ -39,6 +38,8 @@ import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; class GenericEditor extends React.PureComponent { // static propTypes = {onBack: PropTypes.func.isRequired}; diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 504d563bd9..ee3696b427 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import FlairStore from "../../../stores/FlairStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends IDialogProps { communityId: string; @@ -118,7 +119,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent; if (!this.state.avatarPreview) { if (this.state.currentAvatarUrl) { - const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl); + const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp; preview = ; } else { preview =
    diff --git a/src/components/views/dialogs/FeedbackDialog.js b/src/components/views/dialogs/FeedbackDialog.js index cbe26af6cc..d80a935573 100644 --- a/src/components/views/dialogs/FeedbackDialog.js +++ b/src/components/views/dialogs/FeedbackDialog.js @@ -100,6 +100,20 @@ export default (props) => { ); } + let bugReports = null; + if (SdkConfig.get().bug_report_endpoint_url) { + bugReports = ( +

    { + _t("PRO TIP: If you start a bug, please submit debug logs " + + "to help us track down the problem.", {}, { + debugLogsLink: sub => ( + {sub} + ), + }) + }

    + ); + } + return ( { }, }) }

    -

    { - _t("PRO TIP: If you start a bug, please submit debug logs " + - "to help us track down the problem.", {}, { - debugLogsLink: sub => ( - {sub} - ), - }) - }

    + {bugReports}
    { countlyFeedbackSection } } diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index d65ec7563f..f18b7a9d0c 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; const PHASE_START = 0; const PHASE_SHOW_SAS = 1; @@ -123,22 +124,21 @@ export default class IncomingSasDialog extends React.Component { const Spinner = sdk.getComponent("views.elements.Spinner"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId(); + const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId(); let profile; - if (this.state.opponentProfile) { + const oppProfile = this.state.opponentProfile; + if (oppProfile) { + const url = oppProfile.avatar_url + ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio)) + : null; profile =
    - -

    {this.state.opponentProfile.displayname}

    +

    {oppProfile.displayname}

    ; } else if (this.state.opponentProfileError) { profile =
    diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index b28cc1bf41..de0b5b237b 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -22,7 +22,6 @@ import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Pe import DMRoomMap from "../../../utils/DMRoomMap"; import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import SdkConfig from "../../../SdkConfig"; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import * as Email from "../../../email"; import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils"; import {abbreviateUrl} from "../../../utils/UrlUtils"; @@ -43,6 +42,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics"; import {Room} from "matrix-js-sdk/src/models/room"; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -160,9 +160,9 @@ class DMUserTile extends React.PureComponent { width={avatarSize} height={avatarSize} /> : { src={require("../../../../res/img/icon-email-pill-avatar.svg")} width={avatarSize} height={avatarSize} /> : + {_t( + "This usually only affects how the room is processed on the server. If you're " + + "having problems with your %(brand)s, please report a bug.", {brand}, + )} +

    + ); + if (SdkConfig.get().bug_report_endpoint_url) { + bugReports = ( +

    + {_t( + "This usually only affects how the room is processed on the server. If you're " + + "having problems with your %(brand)s, please report a bug.", + { + brand, + }, + { + "a": (sub) => { + return {sub}; + }, + }, + )} +

    + ); + } + return ( -

    - {_t( - "This usually only affects how the room is processed on the server. If you're " + - "having problems with your %(brand)s, please report a bug.", - { - brand, - }, - { - "a": (sub) => { - return {sub}; - }, - }, - )} -

    + {bugReports}

    {_t( "You'll upgrade this room from to .", diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js index 07e29adcff..618b0b4347 100644 --- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js +++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import {Room} from "matrix-js-sdk"; +import {Room} from "matrix-js-sdk/src/models/room"; import * as sdk from '../../../index'; import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms"; import classNames from 'classnames'; diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.js index 72e6c3f3a0..e8625ec6cb 100644 --- a/src/components/views/dialogs/TermsDialog.js +++ b/src/components/views/dialogs/TermsDialog.js @@ -20,8 +20,8 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t, pickBestLanguage } from '../../../languageHandler'; -import Matrix from 'matrix-js-sdk'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; class TermsCheckbox extends React.PureComponent { static propTypes = { @@ -85,22 +85,22 @@ export default class TermsDialog extends React.PureComponent { _nameForServiceType(serviceType, host) { switch (serviceType) { - case Matrix.SERVICE_TYPES.IS: + case SERVICE_TYPES.IS: return

    {_t("Identity Server")}
    ({host})
    ; - case Matrix.SERVICE_TYPES.IM: + case SERVICE_TYPES.IM: return
    {_t("Integration Manager")}
    ({host})
    ; } } _summaryForServiceType(serviceType) { switch (serviceType) { - case Matrix.SERVICE_TYPES.IS: + case SERVICE_TYPES.IS: return
    {_t("Find others by phone or email")}
    {_t("Be found by phone or email")}
    ; - case Matrix.SERVICE_TYPES.IM: + case SERVICE_TYPES.IM: return
    {_t("Use bots, bridges, widgets and sticker packs")}
    ; diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js index ca28ca094c..1fafe03d95 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; -import { MatrixClient } from 'matrix-js-sdk'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import { _t } from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../SecurityManager'; diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 4a216dbae4..df66d10a71 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -19,10 +19,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from "../../../index"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import { UserAddressType } from '../../../UserAddress.js'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.elements.AddressTile") export default class AddressTile extends React.Component { @@ -47,9 +47,7 @@ export default class AddressTile extends React.Component { const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType); if (isMatrixAddress && address.avatarMxc) { - imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp( - address.avatarMxc, 25, 25, 'crop', - )); + imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25)); } else if (address.addressType === 'email') { imgUrls.push(require("../../../../res/img/icon-email-user.svg")); } diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index c539f2be1c..5fd73f974d 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -70,9 +70,7 @@ export default class EventTilePreview extends React.Component { const client = MatrixClientPeg.get(); const userId = client.getUserId(); const profileInfo = await client.getProfileInfo(userId); - const avatarUrl = Avatar.avatarUrlForUser( - {avatarUrl: profileInfo.avatar_url}, - AVATAR_SIZE, AVATAR_SIZE, "crop"); + const avatarUrl = profileInfo.avatar_url; this.setState({ userId, @@ -113,8 +111,9 @@ export default class EventTilePreview extends React.Component { name: displayname, userId: userId, getAvatarUrl: (..._) => { - return avatarUrl; + return Avatar.avatarUrlForUser({avatarUrl}, AVATAR_SIZE, AVATAR_SIZE, "crop"); }, + getMxcAvatarUrl: () => avatarUrl, }; return event; diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 75998cb721..73d5b91511 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -20,6 +20,7 @@ import FlairStore from '../../../stores/FlairStore'; import dis from '../../../dispatcher/dispatcher'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; class FlairAvatar extends React.Component { @@ -39,8 +40,7 @@ class FlairAvatar extends React.Component { } render() { - const httpUrl = this.context.mxcUrlToHttp( - this.props.groupProfile.avatarUrl, 16, 16, 'scale', false); + const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16); const tooltip = this.props.groupProfile.name ? `${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`: this.props.groupProfile.groupId; diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index b0d4fc7fa2..a8e16813e6 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019, 2021 The Matrix.org Foundation C.I.C. +Copyright 2017 - 2019, 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. @@ -19,13 +17,15 @@ import React from 'react'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import classNames from 'classnames'; -import { Room, RoomMember } from 'matrix-js-sdk'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import FlairStore from "../../../stores/FlairStore"; import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; +import {mediaFromMxc} from "../../../customisations/Media"; import Tooltip from './Tooltip'; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -254,12 +254,12 @@ class Pill extends React.Component { case Pill.TYPE_GROUP_MENTION: { if (this.state.group) { const {avatarUrl, groupId, name} = this.state.group; - const cli = MatrixClientPeg.get(); linkText = groupId; if (this.props.shouldShowPillAvatar) { - avatar =
    ); + const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800); + avatarElement =
    ; } const groupRoomName = this.state.groupRoom.displayname; diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 8b25437f71..7edfc1a376 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -21,6 +21,7 @@ import dis from '../../../dispatcher/dispatcher'; import { GroupRoomType } from '../../../groups'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.groups.GroupRoomTile") class GroupRoomTile extends React.Component { @@ -42,10 +43,9 @@ class GroupRoomTile extends React.Component { render() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const avatarUrl = this.context.mxcUrlToHttp( - this.props.groupRoom.avatarUrl, - 36, 36, 'crop', - ); + const avatarUrl = this.props.groupRoom.avatarUrl + ? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36) + : null; const av = ( { profile.shortDescription }
    :
    ; - const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp( - profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null; + const httpUrl = profile.avatarUrl + ? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight) + : null; let avatarElement = (
    diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js index a298f7eb68..e2eda1e12a 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import * as HtmlUtils from '../../../HtmlUtils'; import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils'; import {formatTime} from '../../../DateUtils'; -import {MatrixEvent} from 'matrix-js-sdk'; +import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; import {pillifyLinks, unmountPills} from '../../../utils/pillify'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 498e2db12a..0d5e449fc0 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -17,11 +17,11 @@ import React from 'react'; import MFileBody from './MFileBody'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; @replaceableComponent("views.messages.MAudioBody") export default class MAudioBody extends React.Component { @@ -41,11 +41,11 @@ export default class MAudioBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + return media.srcHttp; } } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index e9893f99b6..8f464e08bd 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2015, 2016, 2018, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,52 +17,24 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import filesize from 'filesize'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {decryptFile} from '../../../utils/DecryptFile'; -import Tinter from '../../../Tinter'; -import request from 'browser-request'; import Modal from '../../../Modal'; import AccessibleButton from "../elements/AccessibleButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; +import ErrorDialog from "../dialogs/ErrorDialog"; +let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on -// A cached tinted copy of require("../../../../res/img/download.svg") -let tintedDownloadImageURL; -// Track a list of mounted MFileBody instances so that we can update -// the require("../../../../res/img/download.svg") when the tint changes. -let nextMountId = 0; -const mounts = {}; - -/** - * Updates the tinted copy of require("../../../../res/img/download.svg") when the tint changes. - */ -function updateTintedDownloadImage() { - // Download the svg as an XML document. - // We could cache the XML response here, but since the tint rarely changes - // it's probably not worth it. - // Also note that we can't use fetch here because fetch doesn't support - // file URLs, which the download image will be if we're running from - // the filesystem (like in an Electron wrapper). - request({uri: require("../../../../res/img/download.svg")}, (err, response, body) => { - if (err) return; - - const svg = new DOMParser().parseFromString(body, "image/svg+xml"); - // Apply the fixups to the XML. - const fixups = Tinter.calcSvgFixups([{contentDocument: svg}]); - Tinter.applySvgFixups(fixups); - // Encoded the fixed up SVG as a data URL. - const svgString = new XMLSerializer().serializeToString(svg); - tintedDownloadImageURL = "data:image/svg+xml;base64," + window.btoa(svgString); - // Notify each mounted MFileBody that the URL has changed. - Object.keys(mounts).forEach(function(id) { - mounts[id].tint(); - }); - }); +async function cacheDownloadIcon() { + if (downloadIconUrl) return; // cached already + const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text()); + downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg); } -Tinter.registerTintable(updateTintedDownloadImage); +// Cache the asset immediately +cacheDownloadIcon(); // User supplied content can contain scripts, we have to be careful that // we don't accidentally run those script within the same origin as the @@ -106,6 +77,7 @@ function computedStyle(element) { } const style = window.getComputedStyle(element, null); let cssText = style.cssText; + // noinspection EqualityComparisonWithCoercionJS if (cssText == "") { // Firefox doesn't implement ".cssText" for computed styles. // https://bugzilla.mozilla.org/show_bug.cgi?id=137687 @@ -145,7 +117,6 @@ export default class MFileBody extends React.Component { this._iframe = createRef(); this._dummyLink = createRef(); - this._downloadImage = createRef(); } /** @@ -178,16 +149,8 @@ export default class MFileBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - return MatrixClientPeg.get().mxcUrlToHttp(content.url); - } - - componentDidMount() { - // Add this to the list of mounted components to receive notifications - // when the tint changes. - this.id = nextMountId++; - mounts[this.id] = this; - this.tint(); + const media = mediaFromContent(this.props.mxEvent.getContent()); + return media.srcHttp; } componentDidUpdate(prevProps, prevState) { @@ -196,34 +159,12 @@ export default class MFileBody extends React.Component { } } - componentWillUnmount() { - // Remove this from the list of mounted components - delete mounts[this.id]; - } - - tint = () => { - // Update our tinted copy of require("../../../../res/img/download.svg") - if (this._downloadImage.current) { - this._downloadImage.current.src = tintedDownloadImageURL; - } - if (this._iframe.current) { - // If the attachment is encrypted then the download image - // will be inside the iframe so we wont be able to update - // it directly. - this._iframe.current.contentWindow.postMessage({ - imgSrc: tintedDownloadImageURL, - style: computedStyle(this._dummyLink.current), - }, "*"); - } - }; - render() { const content = this.props.mxEvent.getContent(); const text = this.presentableTextForFile(content); const isEncrypted = content.file !== undefined; const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); const contentUrl = this._getContentUrl(); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const fileSize = content.info ? content.info.size : null; const fileType = content.info ? content.info.mimetype : "application/octet-stream"; @@ -280,7 +221,8 @@ export default class MFileBody extends React.Component { // When the iframe loads we tell it to render a download link const onIframeLoad = (ev) => { ev.target.contentWindow.postMessage({ - imgSrc: tintedDownloadImageURL, + imgSrc: downloadIconUrl, + imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon. style: computedStyle(this._dummyLink.current), blob: this.state.decryptedBlob, // Set a download attribute for encrypted files so that the file @@ -384,7 +326,7 @@ export default class MFileBody extends React.Component { {placeholder} diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 59c5b4e66b..3683818027 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; @replaceableComponent("views.messages.MImageBody") export default class MImageBody extends React.Component { @@ -70,7 +71,7 @@ export default class MImageBody extends React.Component { this._image = createRef(); } - // FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too! + // FIXME: factor this out and apply it to MVideoBody and MAudioBody too! onClientSync(syncState, prevState) { if (this.unmounted) return; // Consider the client reconnected if there is no error with syncing. @@ -167,16 +168,16 @@ export default class MImageBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return this.context.mxcUrlToHttp(content.url); + return media.srcHttp; } } _getThumbUrl() { - // FIXME: the dharma skin lets images grow as wide as you like, rather than capped to 800x600. + // FIXME: we let images grow as wide as you like, rather than capped to 800x600. // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the // thumbnail resolution will be unnecessarily reduced. // custom timeline widths seems preferable. @@ -185,21 +186,19 @@ export default class MImageBody extends React.Component { const thumbHeight = Math.round(600 * pixelRatio); const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(content); + + if (media.isEncrypted) { // Don't use the thumbnail for clients wishing to autoplay gifs. if (this.state.decryptedThumbnailUrl) { return this.state.decryptedThumbnailUrl; } return this.state.decryptedUrl; - } else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) { + } else if (content.info && content.info.mimetype === "image/svg+xml" && media.hasThumbnail) { // special case to return clientside sender-generated thumbnails for SVGs, if any, // given we deliberately don't thumbnail them serverside to prevent // billion lol attacks and similar - return this.context.mxcUrlToHttp( - content.info.thumbnail_url, - thumbWidth, - thumbHeight, - ); + return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale'); } else { // we try to download the correct resolution // for hi-res images (like retina screenshots). @@ -218,7 +217,7 @@ export default class MImageBody extends React.Component { pixelRatio === 1.0 || (!info || !info.w || !info.h || !info.size) ) { - return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight); + return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } else { // we should only request thumbnails if the image is bigger than 800x600 // (or 1600x1200 on retina) otherwise the image in the timeline will just @@ -233,24 +232,17 @@ export default class MImageBody extends React.Component { info.w > thumbWidth || info.h > thumbHeight ); - const isLargeFileSize = info.size > 1*1024*1024; + const isLargeFileSize = info.size > 1*1024*1024; // 1mb if (isLargeFileSize && isLargerThanThumbnail) { // image is too large physically and bytewise to clutter our timeline so // we ask for a thumbnail, despite knowing that it will be max 800x600 // despite us being retina (as synapse doesn't do 1600x1200 thumbs yet). - return this.context.mxcUrlToHttp( - content.url, - thumbWidth, - thumbHeight, - ); + return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } else { // download the original image otherwise, so we can scale it client side // to take pixelRatio into account. - // ( no width/height means we want the original image) - return this.context.mxcUrlToHttp( - content.url, - ); + return media.srcHttp; } } } diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 89985dee7d..89e661cb2f 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -17,12 +17,12 @@ limitations under the License. import React from 'react'; import MFileBody from './MFileBody'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; interface IProps { /* the MatrixEvent to show */ @@ -76,11 +76,11 @@ export default class MVideoBody extends React.PureComponent { } private getContentUrl(): string|null { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + return media.srcHttp; } } @@ -91,10 +91,11 @@ export default class MVideoBody extends React.PureComponent { private getThumbUrl(): string|null { const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(content); + if (media.isEncrypted) { return this.state.decryptedThumbnailUrl; - } else if (content.info && content.info.thumbnail_url) { - return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url); + } else if (media.hasThumbnail) { + return media.thumbnailHttp; } else { return null; } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index c33debe3f5..5a6e7d87b7 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -18,7 +18,7 @@ limitations under the License. import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; -import { EventStatus } from 'matrix-js-sdk'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index ba860216f0..00aaf9bfda 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -24,6 +24,7 @@ import * as sdk from '../../../index'; import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.messages.RoomAvatarEvent") export default class RoomAvatarEvent extends React.Component { @@ -35,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component { onAvatarClick = () => { const cli = MatrixClientPeg.get(); const ev = this.props.mxEvent; - const httpUrl = cli.mxcUrlToHttp(ev.getContent().url); + const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp; const room = cli.getRoom(this.props.mxEvent.getRoomId()); const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 5fbfd03bd1..12a6a2a311 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -64,6 +64,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IDevice { deviceId: string; @@ -1424,14 +1425,14 @@ const UserInfoHeader: React.FC<{ const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl; if (!avatarUrl) return; - const httpUrl = cli.mxcUrlToHttp(avatarUrl); + const httpUrl = mediaFromMxc(avatarUrl).srcHttp; const params = { src: httpUrl, name: member.name, }; Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); - }, [cli, member]); + }, [member]); const avatarElement = (
    diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js index f82e238722..272ecd1228 100644 --- a/src/components/views/room_settings/RelatedGroupSettings.js +++ b/src/components/views/room_settings/RelatedGroupSettings.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {MatrixEvent} from 'matrix-js-sdk'; +import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js index 563368384b..3dbe2b2b7f 100644 --- a/src/components/views/room_settings/RoomProfileSettings.js +++ b/src/components/views/room_settings/RoomProfileSettings.js @@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import Field from "../elements/Field"; import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; // TODO: Merge with ProfileSettings? @replaceableComponent("views.room_settings.RoomProfileSettings") @@ -38,7 +39,7 @@ export default class RoomProfileSettings extends React.Component { const avatarEvent = room.currentState.getStateEvents("m.room.avatar", ""); let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null; - if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); + if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); const topicEvent = room.currentState.getStateEvents("m.room.topic", ""); const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : ''; @@ -112,7 +113,7 @@ export default class RoomProfileSettings extends React.Component { if (this.state.avatarFile) { const uri = await client.uploadContent(this.state.avatarFile); await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, ''); - newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); + newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); newState.originalAvatarUrl = newState.avatarUrl; newState.avatarFile = null; } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 6ecb2bd549..be04a50798 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -27,7 +27,7 @@ import {parseEvent} from '../../../editor/deserialize'; import {PartCreator} from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import classNames from 'classnames'; -import {EventStatus} from 'matrix-js-sdk'; +import {EventStatus} from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index cbe3252c2b..644d64d322 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -28,7 +28,7 @@ import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {Layout, LayoutPropType} from "../../../settings/Layout"; -import {EventStatus} from 'matrix-js-sdk'; +import {EventStatus} from 'matrix-js-sdk/src/models/event'; import {formatTime} from "../../../DateUtils"; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 39c9f0bcf7..536abf57fc 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -26,6 +26,7 @@ import Modal from "../../../Modal"; import * as ImageUtils from "../../../ImageUtils"; import { _t } from "../../../languageHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.rooms.LinkPreviewWidget") export default class LinkPreviewWidget extends React.Component { @@ -83,7 +84,7 @@ export default class LinkPreviewWidget extends React.Component { let src = p["og:image"]; if (src && src.startsWith("mxc://")) { - src = MatrixClientPeg.get().mxcUrlToHttp(src); + src = mediaFromMxc(src).srcHttp; } const params = { @@ -109,9 +110,11 @@ export default class LinkPreviewWidget extends React.Component { if (!SettingsStore.getValue("showImages")) { image = null; // Don't render a button to show the image, just hide it outright } - const imageMaxWidth = 100; const imageMaxHeight = 100; + const imageMaxWidth = 100; + const imageMaxHeight = 100; if (image && image.startsWith("mxc://")) { - image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); + // We deliberately don't want a square here, so use the source HTTP thumbnail function + image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale'); } let thumbHeight = imageMaxHeight; diff --git a/src/components/views/rooms/RoomDetailRow.js b/src/components/views/rooms/RoomDetailRow.js index e7c259cd98..62960930f2 100644 --- a/src/components/views/rooms/RoomDetailRow.js +++ b/src/components/views/rooms/RoomDetailRow.js @@ -18,10 +18,9 @@ import * as sdk from '../../../index'; import React, {createRef} from 'react'; import { _t } from '../../../languageHandler'; import { linkifyElement } from '../../../HtmlUtils'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; import PropTypes from 'prop-types'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; export function getDisplayAliasForRoom(room) { return room.canonicalAlias || (room.aliases ? room.aliases[0] : ""); @@ -100,13 +99,14 @@ export default class RoomDetailRow extends React.Component { { guestJoin }
    ) :
    ; + let avatarUrl = null; + if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24); + return + url={avatarUrl} />
    { name }
      diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 07de70fe45..79db460275 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -333,6 +333,17 @@ export default class RoomTile extends React.PureComponent { this.setState({generalMenuPosition: null}); // hide the menu }; + private onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + dis.dispatch({ + action: 'view_invite', + roomId: this.props.room.roomId, + }); + this.setState({generalMenuPosition: null}); // hide the menu + }; + private async saveNotifState(ev: ButtonEvent, newState: Volume) { ev.preventDefault(); ev.stopPropagation(); @@ -453,6 +464,8 @@ export default class RoomTile extends React.PureComponent { const isLowPriority = roomTags.includes(DefaultTagID.LowPriority); const lowPriorityLabel = _t("Low Priority"); + const userId = MatrixClientPeg.get().getUserId(); + const canInvite = this.props.room.canInvite(userId); contextMenu = { label={lowPriorityLabel} iconClassName="mx_RoomTile_iconArrowDown" /> - + {canInvite ? ( + + ) : null} { let networkIcon; if (protocol.avatar_url) { - const avatarUrl = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - protocol.avatar_url, 64, 64, "crop", - ); + const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64); networkIcon = ; } interface IItemState { @@ -299,7 +300,8 @@ export class SpaceItem extends React.PureComponent { const isNarrow = this.props.isPanelCollapsed; const collapsed = this.state.collapsed || forceCollapsed; - const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId); + const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId) + .filter(s => !this.props.parents?.has(s.roomId)); const isActive = activeSpaces.includes(space); const itemClasses = classNames({ "mx_SpaceItem": true, @@ -312,11 +314,17 @@ export class SpaceItem extends React.PureComponent { mx_SpaceButton_narrow: isNarrow, }); const notificationState = SpaceStore.instance.getNotificationState(space.roomId); - const childItems = childSpaces && !collapsed ? : null; + + let childItems; + if (childSpaces && !collapsed) { + childItems = ; + } + let notifBadge; if (notificationState) { notifBadge =
    @@ -383,12 +391,14 @@ interface ITreeLevelProps { spaces: Room[]; activeSpaces: Room[]; isNested?: boolean; + parents: Set; } const SpaceTreeLevel: React.FC = ({ spaces, activeSpaces, isNested, + parents, }) => { return
      {spaces.map(s => { @@ -397,6 +407,7 @@ const SpaceTreeLevel: React.FC = ({ activeSpaces={activeSpaces} space={s} isNested={isNested} + parents={parents} />); })}
    ; diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts new file mode 100644 index 0000000000..f262179f3d --- /dev/null +++ b/src/customisations/Media.ts @@ -0,0 +1,144 @@ +/* + * 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 {MatrixClientPeg} from "../MatrixClientPeg"; +import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; +import {ResizeMethod} from "../Avatar"; + +// Populate this class with the details of your customisations when copying it. + +// Implementation note: The Media class must complete the contract as shown here, though +// the constructor can be whatever is relevant to your implementation. The mediaForX +// functions below create an instance of the Media class and are used throughout the +// project. + +/** + * A media object is a representation of a "source media" and an optional + * "thumbnail media", derived from event contents or external sources. + */ +export class Media { + // Per above, this constructor signature can be whatever is helpful for you. + constructor(private prepared: IPreparedMedia) { + } + + /** + * True if the media appears to be encrypted. Actual file contents may vary. + */ + public get isEncrypted(): boolean { + return !!this.prepared.file; + } + + /** + * The MXC URI of the source media. + */ + public get srcMxc(): string { + return this.prepared.mxc; + } + + /** + * The MXC URI of the thumbnail media, if a thumbnail is recorded. Null/undefined + * otherwise. + */ + public get thumbnailMxc(): string | undefined | null { + return this.prepared.thumbnail?.mxc; + } + + /** + * Whether or not a thumbnail is recorded for this media. + */ + public get hasThumbnail(): boolean { + return !!this.thumbnailMxc; + } + + /** + * The HTTP URL for the source media. + */ + public get srcHttp(): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc); + } + + /** + * The HTTP URL for the thumbnail media (without any specified width, height, etc). Null/undefined + * if no thumbnail media recorded. + */ + public get thumbnailHttp(): string | undefined | null { + if (!this.hasThumbnail) return null; + return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc); + } + + /** + * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail + * is recorded for this media. Returns null/undefined otherwise. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null | undefined { + if (!this.hasThumbnail) return null; + return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode); + } + + /** + * Gets the HTTP URL for a thumbnail of the source media with the requested characteristics. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMethod = "scale"): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode); + } + + /** + * Creates a square thumbnail of the media. If the media has a thumbnail recorded, that MXC will + * be used, otherwise the source media will be used. + * @param {number} dim The desired width and height. + * @returns {string} An HTTP URL for the thumbnail. + */ + public getSquareThumbnailHttp(dim: number): string { + if (this.hasThumbnail) { + return this.getThumbnailHttp(dim, dim, 'crop'); + } + return this.getThumbnailOfSourceHttp(dim, dim, 'crop'); + } + + /** + * Downloads the source media. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadSource(): Promise { + return fetch(this.srcHttp); + } +} + +/** + * Creates a media object from event content. + * @param {IMediaEventContent} content The event content. + * @returns {Media} The media object. + */ +export function mediaFromContent(content: IMediaEventContent): Media { + return new Media(prepEventContentAsMedia(content)); +} + +/** + * Creates a media object from an MXC URI. + * @param {string} mxc The MXC URI. + * @returns {Media} The media object. + */ +export function mediaFromMxc(mxc: string): Media { + return mediaFromContent({url: mxc}); +} diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts new file mode 100644 index 0000000000..fb05d76a4d --- /dev/null +++ b/src/customisations/models/IMediaEventContent.ts @@ -0,0 +1,88 @@ +/* + * 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. + */ + +// TODO: These types should be elsewhere. + +export interface IEncryptedFile { + url: string; + mimetype?: string; + key: { + alg: string; + key_ops: string[]; // eslint-disable-line camelcase + kty: string; + k: string; + ext: boolean; + }; + iv: string; + hashes: {[alg: string]: string}; + v: string; +} + +export interface IMediaEventContent { + url?: string; // required on unencrypted media + file?: IEncryptedFile; // required for *encrypted* media + info?: { + thumbnail_url?: string; // eslint-disable-line camelcase + thumbnail_file?: IEncryptedFile; // eslint-disable-line camelcase + }; +} + +export interface IPreparedMedia extends IMediaObject { + thumbnail?: IMediaObject; +} + +export interface IMediaObject { + mxc: string; + file?: IEncryptedFile; +} + +/** + * Parses an event content body into a prepared media object. This prepared media object + * can be used with other functions to manipulate the media. + * @param {IMediaEventContent} content Unredacted media event content. See interface. + * @returns {IPreparedMedia} A prepared media object. + * @throws Throws if the given content cannot be packaged into a prepared media object. + */ +export function prepEventContentAsMedia(content: IMediaEventContent): IPreparedMedia { + let thumbnail: IMediaObject = null; + if (content?.info?.thumbnail_url) { + thumbnail = { + mxc: content.info.thumbnail_url, + file: content.info.thumbnail_file, + }; + } else if (content?.info?.thumbnail_file?.url) { + thumbnail = { + mxc: content.info.thumbnail_file.url, + file: content.info.thumbnail_file, + }; + } + + if (content?.url) { + return { + thumbnail, + mxc: content.url, + file: content.file, + }; + } else if (content?.file?.url) { + return { + thumbnail, + mxc: content.file.url, + file: content.file, + }; + } + + throw new Error("Invalid file provided: cannot determine MXC URI. Has it been redacted?"); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 63449eb99f..63b19831bb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1601,6 +1601,7 @@ "Favourited": "Favourited", "Favourite": "Favourite", "Low Priority": "Low Priority", + "Invite People": "Invite People", "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", @@ -2150,10 +2151,10 @@ "Add comment": "Add comment", "Comment": "Comment", "There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.", + "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", "Feedback": "Feedback", "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", - "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", "Send feedback": "Send feedback", "Confirm abort of host creation": "Confirm abort of host creation", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", @@ -2201,6 +2202,7 @@ "Go": "Go", "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Unnamed Space": "Unnamed Space", + "Invite to %(roomName)s": "Invite to %(roomName)s", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", "Invite someone using their name, username (like ) or share this space.": "Invite someone using their name, username (like ) or share this space.", "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", @@ -2269,8 +2271,9 @@ "Automatically invite users": "Automatically invite users", "Upgrade private room": "Upgrade private room", "Upgrade public room": "Upgrade public room", - "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", + "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", "You'll upgrade this room from to .": "You'll upgrade this room from to .", "Resend": "Resend", "You're all caught up.": "You're all caught up.", @@ -2550,6 +2553,8 @@ "Logout": "Logout", "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", + "%(count)s messages deleted.|other": "%(count)s messages deleted.", + "%(count)s messages deleted.|one": "%(count)s message deleted.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", diff --git a/src/index.js b/src/index.js index 008e15ad90..e360c04f4f 100644 --- a/src/index.js +++ b/src/index.js @@ -28,3 +28,7 @@ export function resetSkin() { export function getComponent(componentName) { return Skinner.getComponent(componentName); } + +// Import the js-sdk so the proper `request` object can be set. This does some +// magic with the browser injection to make all subsequent imports work fine. +import "matrix-js-sdk"; diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index fa263a2a55..2dcdb9e3a3 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -16,7 +16,8 @@ limitations under the License. import PlatformPeg from "../PlatformPeg"; import {MatrixClientPeg} from "../MatrixClientPeg"; -import {EventTimeline, RoomMember} from 'matrix-js-sdk'; +import {RoomMember} from 'matrix-js-sdk/src/models/room-member'; +import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; import {sleep} from "../utils/promise"; import SettingsStore from "../settings/SettingsStore"; import {EventEmitter} from "events"; diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 8eb77bb3ae..b886f369df 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -434,15 +434,37 @@ function selectQuery(store, keyRange, resultMapper) { /** * Configure rage shaking support for sending bug reports. * Modifies globals. + * @param {boolean} setUpPersistence When true (default), the persistence will + * be set up immediately for the logs. * @return {Promise} Resolves when set up. */ -export function init() { +export function init(setUpPersistence = true) { if (global.mx_rage_initPromise) { return global.mx_rage_initPromise; } global.mx_rage_logger = new ConsoleLogger(); global.mx_rage_logger.monkeyPatch(window.console); + if (setUpPersistence) { + return tryInitStorage(); + } + + global.mx_rage_initPromise = Promise.resolve(); + return global.mx_rage_initPromise; +} + +/** + * Try to start up the rageshake storage for logs. If not possible (client unsupported) + * then this no-ops. + * @return {Promise} Resolves when complete. + */ +export function tryInitStorage() { + if (global.mx_rage_initStoragePromise) { + return global.mx_rage_initStoragePromise; + } + + console.log("Configuring rageshake persistence..."); + // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. let indexedDB; @@ -452,11 +474,11 @@ export function init() { if (indexedDB) { global.mx_rage_store = new IndexedDBLogStore(indexedDB, global.mx_rage_logger); - global.mx_rage_initPromise = global.mx_rage_store.connect(); - return global.mx_rage_initPromise; + global.mx_rage_initStoragePromise = global.mx_rage_store.connect(); + return global.mx_rage_initStoragePromise; } - global.mx_rage_initPromise = Promise.resolve(); - return global.mx_rage_initPromise; + global.mx_rage_initStoragePromise = Promise.resolve(); + return global.mx_rage_initStoragePromise; } export function flush() { diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index 8983380fec..bb45456f1e 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -22,19 +22,29 @@ import { User } from "matrix-js-sdk/src/models/user"; import { throttle } from "lodash"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; +import {mediaFromMxc} from "../customisations/Media"; interface IState { displayName?: string; avatarUrl?: string; } +const KEY_DISPLAY_NAME = "mx_profile_displayname"; +const KEY_AVATAR_URL = "mx_profile_avatar_url"; + export class OwnProfileStore extends AsyncStoreWithClient { private static internalInstance = new OwnProfileStore(); private monitoredUser: User; private constructor() { - super(defaultDispatcher, {}); + // seed from localstorage because otherwise we won't get these values until a whole network + // round-trip after the client is ready, and we often load widgets in that time, and we'd + // and up passing them an incorrect display name + super(defaultDispatcher, { + displayName: window.localStorage.getItem(KEY_DISPLAY_NAME), + avatarUrl: window.localStorage.getItem(KEY_AVATAR_URL), + }); } public static get instance(): OwnProfileStore { @@ -72,8 +82,12 @@ export class OwnProfileStore extends AsyncStoreWithClient { */ public getHttpAvatarUrl(size = 0): string { if (!this.avatarMxc) return null; - const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through - return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize); + const media = mediaFromMxc(this.avatarMxc); + if (!size || size <= 0) { + return media.srcHttp; + } else { + return media.getSquareThumbnailHttp(size); + } } protected async onNotReady() { @@ -110,6 +124,16 @@ export class OwnProfileStore extends AsyncStoreWithClient { // We specifically do not use the User object we stored for profile info as it // could easily be wrong (such as per-room instead of global profile). const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId()); + if (profileInfo.displayname) { + window.localStorage.setItem(KEY_DISPLAY_NAME, profileInfo.displayname); + } else { + window.localStorage.removeItem(KEY_DISPLAY_NAME); + } + if (profileInfo.avatar_url) { + window.localStorage.setItem(KEY_AVATAR_URL, profileInfo.avatar_url); + } else { + window.localStorage.removeItem(KEY_AVATAR_URL); + } await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url}); }; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 3f415f946d..074c2e569d 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -655,6 +655,18 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (!algorithmTags) return [DefaultTagID.Untagged]; return algorithmTags; } + + /** + * Manually update a room with a given cause. This should only be used if the + * room list store would otherwise be incapable of doing the update itself. Note + * that this may race with the room list's regular operation. + * @param {Room} room The room to update. + * @param {RoomUpdateCause} cause The cause to update for. + */ + public async manualRoomUpdate(room: Room, cause: RoomUpdateCause) { + await this.handleRoomUpdate(room, cause); + this.updateFn.trigger(); + } } export default class RoomListStore { diff --git a/src/usercontent/index.js b/src/usercontent/index.js index 6ecd17dcd7..13f38cc31a 100644 --- a/src/usercontent/index.js +++ b/src/usercontent/index.js @@ -1,10 +1,8 @@ function remoteRender(event) { const data = event.data; - const img = document.createElement("img"); + const img = document.createElement("span"); // we'll mask it as an image img.id = "img"; - img.src = data.imgSrc; - img.style = data.imgStyle; const a = document.createElement("a"); a.id = "a"; @@ -16,6 +14,23 @@ function remoteRender(event) { a.appendChild(img); a.appendChild(document.createTextNode(data.textContent)); + // Apply image style after so we can steal the anchor's colour. + // Style copied from a rendered version of mx_MFileBody_download_icon + img.style = (data.imgStyle || "" + + "width: 12px; height: 12px;" + + "-webkit-mask-size: 12px;" + + "mask-size: 12px;" + + "-webkit-mask-position: center;" + + "mask-position: center;" + + "-webkit-mask-repeat: no-repeat;" + + "mask-repeat: no-repeat;" + + "display: inline-block;") + "" + + + // Always add these styles + `-webkit-mask-image: url('${data.imgSrc}');` + + `mask-image: url('${data.imgSrc}');` + + `background-color: ${a.style.color};`; + const body = document.body; // Don't display scrollbars if the link takes more than one line to display. body.style = "margin: 0px; overflow: hidden"; @@ -26,20 +41,8 @@ function remoteRender(event) { } } -function remoteSetTint(event) { - const data = event.data; - - const img = document.getElementById("img"); - img.src = data.imgSrc; - img.style = data.imgStyle; - - const a = document.getElementById("a"); - a.style = data.style; -} - window.onmessage = function(e) { if (e.origin === window.location.origin) { if (e.data.blob) remoteRender(e); - else remoteSetTint(e); } }; diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 18b6451d3e..614aa4cea8 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import {AutoDiscovery} from "matrix-js-sdk"; +import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery"; import {_t, _td, newTranslatableError} from "../languageHandler"; import {makeType} from "./TypeUtils"; import SdkConfig from '../SdkConfig'; diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.ts similarity index 74% rename from src/utils/DecryptFile.js rename to src/utils/DecryptFile.ts index d3625d614a..93cedbc707 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2016, 2018, 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. @@ -17,8 +16,8 @@ limitations under the License. // Pull in the encryption lib so that we can decrypt attachments. import encrypt from 'browser-encrypt-attachment'; -// Grab the client so that we can turn mxc:// URLs into https:// URLS. -import {MatrixClientPeg} from '../MatrixClientPeg'; +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() @@ -54,48 +53,46 @@ import {MatrixClientPeg} from '../MatrixClientPeg'; // 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': true, - 'image/gif': true, - 'image/png': true, +const ALLOWED_BLOB_MIMETYPES = [ + 'image/jpeg', + 'image/gif', + 'image/png', - 'video/mp4': true, - 'video/webm': true, - 'video/ogg': true, + 'video/mp4', + 'video/webm', + 'video/ogg', - 'audio/mp4': true, - 'audio/webm': true, - 'audio/aac': true, - 'audio/mpeg': true, - 'audio/ogg': true, - 'audio/wave': true, - 'audio/wav': true, - 'audio/x-wav': true, - 'audio/x-pn-wav': true, - 'audio/flac': true, - 'audio/x-flac': true, -}; + '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', +]; /** * Decrypt a file attached to a matrix event. - * @param {Object} file The json taken from the matrix event. + * @param {IEncryptedFile} file The json taken from the matrix event. * This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments} * as the encryption info object, so will also have the those keys in addition to * the keys below. - * @param {string} file.url An mxc:// URL for the encrypted file. - * @param {string} file.mimetype The MIME-type of the plaintext file. - * @returns {Promise} + * @returns {Promise} Resolves to a Blob of the file. */ -export function decryptFile(file) { - const url = MatrixClientPeg.get().mxcUrlToHttp(file.url); +export function decryptFile(file: IEncryptedFile): Promise { + const media = mediaFromContent({file}); // Download the encrypted file as an array buffer. - return Promise.resolve(fetch(url)).then(function(response) { + return media.downloadSource().then((response) => { return response.arrayBuffer(); - }).then(function(responseData) { + }).then((responseData) => { // Decrypt the array buffer using the information taken from // the event content. return encrypt.decryptAttachment(responseData, file); - }).then(function(dataArray) { + }).then((dataArray) => { // Turn the array into a Blob and give it the correct MIME-type. // IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise @@ -103,11 +100,10 @@ export function decryptFile(file) { // 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[mimetype]) { + if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { mimetype = 'application/octet-stream'; } - const blob = new Blob([dataArray], {type: mimetype}); - return blob; + return new Blob([dataArray], {type: mimetype}); }); } diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 6558a11ed4..be21896417 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventStatus } from 'matrix-js-sdk'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; import {MatrixClientPeg} from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; /** diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.js index 093d4eeabf..5ece308954 100644 --- a/src/utils/IdentityServerUtils.js +++ b/src/utils/IdentityServerUtils.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SERVICE_TYPES } from 'matrix-js-sdk'; +import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types'; import SdkConfig from '../SdkConfig'; import {MatrixClientPeg} from '../MatrixClientPeg'; diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js index c90281bacf..23c27a2d1c 100644 --- a/src/utils/StorageManager.js +++ b/src/utils/StorageManager.js @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Matrix from 'matrix-js-sdk'; import {LocalStorageCryptoStore} from 'matrix-js-sdk/src/crypto/store/localStorage-crypto-store'; import Analytics from '../Analytics'; +import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb"; +import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; const localStorage = window.localStorage; @@ -132,7 +133,7 @@ export async function checkConsistency() { async function checkSyncStore() { let exists = false; try { - exists = await Matrix.IndexedDBStore.exists( + exists = await IndexedDBStore.exists( indexedDB, SYNC_STORE_NAME, ); log(`Sync store using IndexedDB contains data? ${exists}`); @@ -148,7 +149,7 @@ async function checkSyncStore() { async function checkCryptoStore() { let exists = false; try { - exists = await Matrix.IndexedDBCryptoStore.exists( + exists = await IndexedDBCryptoStore.exists( indexedDB, CRYPTO_STORE_NAME, ); log(`Crypto store using IndexedDB contains data? ${exists}`); diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.js index c8ff35a584..f5e196d846 100644 --- a/src/utils/createMatrixClient.js +++ b/src/utils/createMatrixClient.js @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Matrix from 'matrix-js-sdk'; +import {createClient} from "matrix-js-sdk/src/matrix"; +import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; +import {WebStorageSessionStore} from "matrix-js-sdk/src/store/session/webstorage"; +import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb"; const localStorage = window.localStorage; @@ -44,7 +47,7 @@ export default function createMatrixClient(opts) { }; if (indexedDB && localStorage) { - storeOpts.store = new Matrix.IndexedDBStore({ + storeOpts.store = new IndexedDBStore({ indexedDB: indexedDB, dbName: "riot-web-sync", localStorage: localStorage, @@ -53,18 +56,18 @@ export default function createMatrixClient(opts) { } if (localStorage) { - storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); + storeOpts.sessionStore = new WebStorageSessionStore(localStorage); } if (indexedDB) { - storeOpts.cryptoStore = new Matrix.IndexedDBCryptoStore( + storeOpts.cryptoStore = new IndexedDBCryptoStore( indexedDB, "matrix-js-sdk:crypto", ); } opts = Object.assign(storeOpts, opts); - return Matrix.createClient(opts); + return createClient(opts); } createMatrixClient.indexedDbWorkerScript = null; diff --git a/src/utils/pages.js b/src/utils/pages.ts similarity index 68% rename from src/utils/pages.js rename to src/utils/pages.ts index d63ca3f2c7..bae76be29d 100644 --- a/src/utils/pages.js +++ b/src/utils/pages.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 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. @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function getHomePageUrl(appConfig) { +import { ConfigOptions } from "../SdkConfig"; + +export function getHomePageUrl(appConfig: ConfigOptions): string | null { const pagesConfig = appConfig.embeddedPages; - let pageUrl = null; - if (pagesConfig) { - pageUrl = pagesConfig.homeUrl; - } + let pageUrl = pagesConfig?.homeUrl; + if (!pageUrl) { // This is a deprecated config option for the home page // (despite the name, given we also now have a welcome @@ -29,3 +29,8 @@ export function getHomePageUrl(appConfig) { return pageUrl; } + +export function shouldUseLoginForWelcome(appConfig: ConfigOptions): boolean { + const pagesConfig = appConfig.embeddedPages; + return pagesConfig?.loginForWelcome === true; +} diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index fb942d2f7c..ee5d1b6912 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -262,7 +262,8 @@ describe('GroupView', function() { expect(longDescElement.innerHTML).toContain('
      '); expect(longDescElement.innerHTML).toContain('
    • And lists!
    • '); - const imgSrc = "https://my.home.server/_matrix/media/r0/thumbnail/someimageurl?width=800&height=600"; + const imgSrc = "https://my.home.server/_matrix/media/r0/thumbnail/someimageurl" + + "?width=800&height=600&method=scale"; expect(longDescElement.innerHTML).toContain(''); }); diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 2fd5bd6ad1..7347ff2658 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -116,6 +116,7 @@ describe('MessagePanel', function() { getAvatarUrl: () => { return "avatar.jpeg"; }, + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', }, ts: ts0 + i*1000, mship: 'join', @@ -148,6 +149,7 @@ describe('MessagePanel', function() { getAvatarUrl: () => { return "avatar.jpeg"; }, + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', }, ts: ts0 + i*1000, mship: 'join', @@ -193,6 +195,7 @@ describe('MessagePanel', function() { getAvatarUrl: () => { return "avatar.jpeg"; }, + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', }, ts: ts0 + 1, mship: 'join', @@ -239,6 +242,7 @@ describe('MessagePanel', function() { getAvatarUrl: () => { return "avatar.jpeg"; }, + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', }, ts: ts0 + 5, mship: 'invite', diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 6d26fa36e9..dd6febc7d7 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -50,6 +50,7 @@ describe('MemberEventListSummary', function() { getAvatarUrl: () => { return "avatar.jpeg"; }, + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', }, }); // Override random event ID to allow for equality tests against tiles from diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index a596825c09..0a6d47a72b 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -37,6 +37,7 @@ describe("", () => { getRoom: () => mkStubRoom("room_id"), getAccountData: () => undefined, isGuest: () => false, + mxcUrlToHttp: (s) => s, }; const ev = mkEvent({ @@ -61,6 +62,7 @@ describe("", () => { getRoom: () => mkStubRoom("room_id"), getAccountData: () => undefined, isGuest: () => false, + mxcUrlToHttp: (s) => s, }; const ev = mkEvent({ @@ -86,6 +88,7 @@ describe("", () => { getRoom: () => mkStubRoom("room_id"), getAccountData: () => undefined, isGuest: () => false, + mxcUrlToHttp: (s) => s, }; }); @@ -139,6 +142,7 @@ describe("", () => { on: () => undefined, removeListener: () => undefined, isGuest: () => false, + mxcUrlToHttp: (s) => s, }; }); @@ -284,6 +288,7 @@ describe("", () => { getAccountData: () => undefined, getUrlPreview: (url) => new Promise(() => {}), isGuest: () => false, + mxcUrlToHttp: (s) => s, }; const ev = mkEvent({ diff --git a/test/setupTests.js b/test/setupTests.js index 9c2d16a8df..6d37d48987 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -2,3 +2,5 @@ import * as languageHandler from "../src/languageHandler"; languageHandler.setLanguage('en'); languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]); + +require('jest-fetch-mock').enableMocks(); diff --git a/test/test-utils.js b/test/test-utils.js index b6e0468d86..d259fcb95f 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -213,6 +213,7 @@ export function mkStubRoom(roomId = null) { rawDisplayName: 'Member', roomId: roomId, getAvatarUrl: () => 'mxc://avatar.url/image.png', + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', }), getMembersWithMembership: jest.fn().mockReturnValue([]), getJoinedMembers: jest.fn().mockReturnValue([]), @@ -242,6 +243,7 @@ export function mkStubRoom(roomId = null) { removeListener: jest.fn(), getDMInviter: jest.fn(), getAvatarUrl: () => 'mxc://avatar.url/room.png', + getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', }; } diff --git a/yarn.lock b/yarn.lock index f99ea5900d..58686248f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2589,6 +2589,13 @@ crc-32@^0.3.0: resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-0.3.0.tgz#6a3d3687f5baec41f7e9b99fe1953a2e5d19775e" integrity sha1-aj02h/W67EH36bmf4ZU6Ll0Zd14= +cross-fetch@^3.0.4: + version "3.0.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" + integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -4918,6 +4925,14 @@ jest-environment-node@^26.6.2: jest-mock "^26.6.2" jest-util "^26.6.2" +jest-fetch-mock@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b" + integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw== + dependencies: + cross-fetch "^3.0.4" + promise-polyfill "^8.1.3" + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" @@ -5573,8 +5588,8 @@ mathml-tag-names@^2.1.3: integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "9.8.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fb73ab687826e4d05fb8b424ab013a771213f84f" + version "9.9.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/cd38fb9b4c349eb31feac14e806e710bf6431b72" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -5835,6 +5850,11 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-fetch@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -6448,6 +6468,11 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-polyfill@^8.1.3: + version "8.2.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0" + integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g== + promise@^7.0.3, promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"