diff --git a/package.json b/package.json index 55b06cac8d..2136de8a61 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.16", + "matrix-widget-api": "^0.1.0-beta.17", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 1c8f4230b6..faeb91da3e 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -92,6 +92,10 @@ limitations under the License. &[data-self=false] { .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); + + .mx_MImageBody .mx_MImageBody_thumbnail { + border-bottom-right-radius: var(--cornerRadius); + } } .mx_EventTile_avatar { left: -34px; @@ -106,12 +110,16 @@ limitations under the License. } &[data-self=true] { .mx_EventTile_line { - border-bottom-left-radius: var(--cornerRadius); float: right; + border-bottom-left-radius: var(--cornerRadius); > a { left: auto; right: -68px; } + + .mx_MImageBody .mx_MImageBody_thumbnail { + border-bottom-left-radius: var(--cornerRadius); + } } .mx_ThreadInfo { @@ -147,33 +155,62 @@ limitations under the License. .mx_EventTile_line { position: relative; - padding: var(--gutterSize); - border-top-left-radius: var(--cornerRadius); - border-top-right-radius: var(--cornerRadius); - background: var(--backgroundColor); display: flex; gap: 5px; margin: 0 -12px 0 -9px; + border-top-left-radius: var(--cornerRadius); + border-top-right-radius: var(--cornerRadius); > a { position: absolute; padding: 10px 20px; top: 0; left: -68px; } + + //noinspection CssReplaceWithShorthandSafely + .mx_MImageBody .mx_MImageBody_thumbnail { + // Note: This is intentionally not compressed because the browser gets confused + // when it is all combined. We're effectively unsetting the border radius then + // setting the two corners we care about manually. + border-radius: unset; + border-top-left-radius: var(--cornerRadius); + border-top-right-radius: var(--cornerRadius); + } + } + + .mx_EventTile_line:not(.mx_EventTile_mediaLine) { + padding: var(--gutterSize); + background: var(--backgroundColor); } &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line { border-top-left-radius: 0; + + .mx_MImageBody .mx_MImageBody_thumbnail { + border-top-left-radius: 0; + } } &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line { border-bottom-left-radius: var(--cornerRadius); + + .mx_MImageBody .mx_MImageBody_thumbnail { + border-bottom-left-radius: var(--cornerRadius); + } } &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line { border-top-right-radius: 0; + + .mx_MImageBody .mx_MImageBody_thumbnail { + border-top-right-radius: 0; + } } &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); + + .mx_MImageBody .mx_MImageBody_thumbnail { + border-bottom-right-radius: var(--cornerRadius); + } } .mx_EventTile_avatar { diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.scss b/res/themes/light-high-contrast/css/_light-high-contrast.scss index f1f4387a06..ef23fdfef1 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.scss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.scss @@ -2,6 +2,7 @@ $accent: #268075; $alert: #D62C25; $links: #0A6ECA; +$primary-content: #17191C; $secondary-content: #5E6266; $tertiary-content: $secondary-content; $quaternary-content: $secondary-content; @@ -106,3 +107,11 @@ $roomtopic-color: $secondary-content; .mx_FontScalingPanel_fontSlider { background-color: $roomlist-button-bg-color !important; } + +.mx_ThemeChoicePanel > .mx_ThemeSelectors > .mx_RadioButton input[type="radio"]:disabled + div { + border-color: $primary-content; +} + +.mx_ThemeChoicePanel > .mx_ThemeSelectors > .mx_RadioButton.mx_RadioButton_disabled { + color: $primary-content; +} diff --git a/src/@types/browser-encrypt-attachment.ts b/src/@types/browser-encrypt-attachment.ts new file mode 100644 index 0000000000..a8249ab350 --- /dev/null +++ b/src/@types/browser-encrypt-attachment.ts @@ -0,0 +1,38 @@ +/* +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. +*/ + +declare module "browser-encrypt-attachment" { + interface IInfo { + v: string; + key: { + alg: string; + key_ops: string[]; // eslint-disable-line camelcase + kty: string; + k: string; + ext: boolean; + }; + iv: string; + hashes: {[alg: string]: string}; + } + + interface IEncryptedAttachment { + data: ArrayBuffer; + info: IInfo; + } + + export function encryptAttachment(plaintextBuffer: ArrayBuffer): Promise; + export function decryptAttachment(ciphertextBuffer: ArrayBuffer, info: IInfo): Promise; +} diff --git a/src/@types/png-chunks-extract.ts b/src/@types/png-chunks-extract.ts new file mode 100644 index 0000000000..697e68d343 --- /dev/null +++ b/src/@types/png-chunks-extract.ts @@ -0,0 +1,26 @@ +/* +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. +*/ + +declare module "png-chunks-extract" { + interface IChunk { + name: string; + data: Uint8Array; + } + + function extractPngChunks(data: Uint8Array | Buffer): IChunk[]; + + export default extractPngChunks; +} diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index abb2ca106d..b85c0462b5 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -18,16 +18,17 @@ limitations under the License. import React from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import encrypt from "browser-encrypt-attachment"; -import extractPngChunks from "png-chunks-extract"; -import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; -import { logger } from "matrix-js-sdk/src/logger"; +import { IEncryptedFile, IMediaEventInfo } from "./customisations/models/IMediaEventContent"; +import { IUploadOpts } from "matrix-js-sdk/src/@types/requests"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import RoomViewStore from './stores/RoomViewStore'; +import encrypt from "browser-encrypt-attachment"; +import extractPngChunks from "png-chunks-extract"; import Spinner from "./components/views/elements/Spinner"; import { Action } from "./dispatcher/actions"; import CountlyAnalytics from "./CountlyAnalytics"; @@ -39,10 +40,13 @@ import { UploadStartedPayload, } from "./dispatcher/payloads/UploadPayload"; import { IUpload } from "./models/IUpload"; +import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; import { BlurhashEncoder } from "./BlurhashEncoder"; import SettingsStore from "./settings/SettingsStore"; import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics"; +import { logger } from "matrix-js-sdk/src/logger"; + const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -306,7 +310,7 @@ function loadVideoElement(videoFile): Promise { function infoForVideoFile(matrixClient, roomId, videoFile) { const thumbnailType = "image/jpeg"; - let videoInfo; + let videoInfo: Partial; return loadVideoElement(videoFile).then((video) => { return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); }).then((result) => { @@ -355,49 +359,48 @@ export function uploadFile( matrixClient: MatrixClient, roomId: string, file: File | Blob, - progressHandler?: any, // TODO: Types -): IAbortablePromise<{url?: string, file?: any}> { // TODO: Types + progressHandler?: IUploadOpts["progressHandler"], +): IAbortablePromise<{ url?: string, file?: IEncryptedFile }> { let canceled = false; if (matrixClient.isRoomEncrypted(roomId)) { // If the room is encrypted then encrypt the file before uploading it. // First read the file into memory. - let uploadPromise; - let encryptInfo; + let uploadPromise: IAbortablePromise; const prom = readFileAsArrayBuffer(file).then(function(data) { if (canceled) throw new UploadCanceledError(); // Then encrypt the file. return encrypt.encryptAttachment(data); }).then(function(encryptResult) { if (canceled) throw new UploadCanceledError(); - // Record the information needed to decrypt the attachment. - encryptInfo = encryptResult.info; + // Pass the encrypted data as a Blob to the uploader. const blob = new Blob([encryptResult.data]); uploadPromise = matrixClient.uploadContent(blob, { - progressHandler: progressHandler, + progressHandler, includeFilename: false, }); - return uploadPromise; - }).then(function(url) { - if (canceled) throw new UploadCanceledError(); - // If the attachment is encrypted then bundle the URL along - // with the information needed to decrypt the attachment and - // add it under a file key. - encryptInfo.url = url; - if (file.type) { - encryptInfo.mimetype = file.type; - } - return { "file": encryptInfo }; - }) as IAbortablePromise<{ file: any }>; + + return uploadPromise.then(url => { + if (canceled) throw new UploadCanceledError(); + + // If the attachment is encrypted then bundle the URL along + // with the information needed to decrypt the attachment and + // add it under a file key. + return { + file: { + ...encryptResult.info, + url, + }, + }; + }); + }) as IAbortablePromise<{ file: IEncryptedFile }>; prom.abort = () => { canceled = true; if (uploadPromise) matrixClient.cancelUpload(uploadPromise); }; return prom; } else { - const basePromise = matrixClient.uploadContent(file, { - progressHandler: progressHandler, - }); + const basePromise = matrixClient.uploadContent(file, { progressHandler }); const promise1 = basePromise.then(function(url) { if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. @@ -553,29 +556,29 @@ export default class ContentMessages { const prom = new Promise((resolve) => { if (file.type.indexOf('image/') === 0) { - content.msgtype = 'm.image'; + content.msgtype = MsgType.Image; infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { Object.assign(content.info, imageInfo); resolve(); }, (e) => { logger.error(e); - content.msgtype = 'm.file'; + content.msgtype = MsgType.File; resolve(); }); } else if (file.type.indexOf('audio/') === 0) { - content.msgtype = 'm.audio'; + content.msgtype = MsgType.Audio; resolve(); } else if (file.type.indexOf('video/') === 0) { - content.msgtype = 'm.video'; + content.msgtype = MsgType.Video; infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { Object.assign(content.info, videoInfo); resolve(); }, (e) => { - content.msgtype = 'm.file'; + content.msgtype = MsgType.File; resolve(); }); } else { - content.msgtype = 'm.file'; + content.msgtype = MsgType.File; resolve(); } }) as IAbortablePromise; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index a0760b6d8c..390c27cf54 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -33,7 +33,7 @@ import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; import LogoutDialog from "../views/dialogs/LogoutDialog"; import SettingsStore from "../../settings/SettingsStore"; -import { getCustomTheme } from "../../theme"; +import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import SdkConfig from "../../SdkConfig"; import { getHomePageUrl } from "../../utils/pages"; @@ -70,6 +70,7 @@ type PartialDOMRect = Pick; interface IState { contextMenuPosition: PartialDOMRect; isDarkTheme: boolean; + isHighContrast: boolean; selectedSpace?: Room; pendingRoomJoin: Set; } @@ -88,6 +89,7 @@ export default class UserMenu extends React.Component { this.state = { contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), + isHighContrast: this.isUserOnHighContrastTheme(), pendingRoomJoin: new Set(), }; @@ -143,6 +145,18 @@ export default class UserMenu extends React.Component { } } + private isUserOnHighContrastTheme(): boolean { + if (SettingsStore.getValue("use_system_theme")) { + return window.matchMedia("(prefers-contrast: more)").matches; + } else { + const theme = SettingsStore.getValue("theme"); + if (theme.startsWith("custom-")) { + return false; + } + return isHighContrastTheme(theme); + } + } + private onProfileUpdate = async () => { // the store triggered an update, so force a layout update. We don't // have any state to store here for that to magically happen. @@ -154,7 +168,11 @@ export default class UserMenu extends React.Component { }; private onThemeChanged = () => { - this.setState({ isDarkTheme: this.isUserOnDarkTheme() }); + this.setState( + { + isDarkTheme: this.isUserOnDarkTheme(), + isHighContrast: this.isUserOnHighContrastTheme(), + }); }; private onAction = (ev: ActionPayload) => { @@ -222,7 +240,13 @@ export default class UserMenu extends React.Component { // Disable system theme matching if the user hits this button SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); - const newTheme = this.state.isDarkTheme ? "light" : "dark"; + let newTheme = this.state.isDarkTheme ? "light" : "dark"; + if (this.state.isHighContrast) { + const hcTheme = findHighContrastTheme(newTheme); + if (hcTheme) { + newTheme = hcTheme; + } + } SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab }; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index e45a292e3d..bbe2475215 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -86,6 +86,7 @@ interface IState { error: Error; menuDisplayed: boolean; widgetPageTitle: string; + requiresClient: boolean; } @replaceableComponent("views.elements.AppTile") @@ -114,8 +115,10 @@ export default class AppTile extends React.Component { this.persistKey = getPersistKey(this.props.app.id); try { this.sgWidget = new StopGapWidget(this.props); - this.sgWidget.on("preparing", this.onWidgetPrepared); + this.sgWidget.on("preparing", this.onWidgetPreparing); this.sgWidget.on("ready", this.onWidgetReady); + // emits when the capabilites have been setup or changed + this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified); } catch (e) { logger.log("Failed to construct widget", e); this.sgWidget = null; @@ -155,6 +158,10 @@ export default class AppTile extends React.Component { error: null, menuDisplayed: false, widgetPageTitle: this.props.widgetPageTitle, + // requiresClient is initially set to true. This avoids the broken state of the popout + // button being visible (for an instance) and then disappearing when the widget is loaded. + // requiresClient <-> hide the popout button + requiresClient: true, }; } @@ -216,7 +223,7 @@ export default class AppTile extends React.Component { } try { this.sgWidget = new StopGapWidget(newProps); - this.sgWidget.on("preparing", this.onWidgetPrepared); + this.sgWidget.on("preparing", this.onWidgetPreparing); this.sgWidget.on("ready", this.onWidgetReady); this.startWidget(); } catch (e) { @@ -287,7 +294,7 @@ export default class AppTile extends React.Component { if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true }); } - private onWidgetPrepared = (): void => { + private onWidgetPreparing = (): void => { this.setState({ loading: false }); }; @@ -297,6 +304,12 @@ export default class AppTile extends React.Component { } }; + private onWidgetCapabilitiesNotified = (): void => { + this.setState({ + requiresClient: this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.RequiresClient), + }); + }; + private onAction = (payload): void => { if (payload.widgetId === this.props.app.id) { switch (payload.action) { @@ -512,7 +525,7 @@ export default class AppTile extends React.Component { { this.props.showTitle && this.getTileTitle() } - { this.props.showPopout && { } const EventTileType = sdk.getComponent(tileHandler); + const isProbablyMedia = MediaEventHelper.isEligible(this.props.mxEvent); + + const lineClasses = classNames({ + mx_EventTile_line: true, + mx_EventTile_mediaLine: isProbablyMedia, + }); const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; @@ -1208,7 +1215,7 @@ export default class EventTile extends React.Component { { timestamp } , -
+
{ { timestamp }
, -
+
{ replyChain } { "aria-atomic": true, "data-scroll-tokens": scrollToken, }, [ -
+
{ { sender } { ircPadlock } { avatar } -
+
{ groupTimestamp } { groupPadlock } { replyChain } diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts index a3e0231ebc..899d03f02d 100644 --- a/src/customisations/models/IMediaEventContent.ts +++ b/src/customisations/models/IMediaEventContent.ts @@ -18,7 +18,6 @@ export interface IEncryptedFile { url: string; - mimetype?: string; key: { alg: string; key_ops: string[]; // eslint-disable-line camelcase diff --git a/src/settings/watchers/ThemeWatcher.ts b/src/settings/watchers/ThemeWatcher.ts index b5e598f667..a8994d6f74 100644 --- a/src/settings/watchers/ThemeWatcher.ts +++ b/src/settings/watchers/ThemeWatcher.ts @@ -21,7 +21,7 @@ import SettingsStore from '../SettingsStore'; import dis from '../../dispatcher/dispatcher'; import { Action } from '../../dispatcher/actions'; import ThemeController from "../controllers/ThemeController"; -import { setTheme } from "../../theme"; +import { findHighContrastTheme, setTheme } from "../../theme"; import { ActionPayload } from '../../dispatcher/payloads'; import { SettingLevel } from "../SettingLevel"; @@ -32,6 +32,7 @@ export default class ThemeWatcher { private preferDark: MediaQueryList; private preferLight: MediaQueryList; + private preferHighContrast: MediaQueryList; private currentTheme: string; @@ -44,6 +45,7 @@ export default class ThemeWatcher { // we can get the tristate of dark/light/unsupported this.preferDark = (global).matchMedia("(prefers-color-scheme: dark)"); this.preferLight = (global).matchMedia("(prefers-color-scheme: light)"); + this.preferHighContrast = (global).matchMedia("(prefers-contrast: more)"); this.currentTheme = this.getEffectiveTheme(); } @@ -54,6 +56,7 @@ export default class ThemeWatcher { if (this.preferDark.addEventListener) { this.preferDark.addEventListener('change', this.onChange); this.preferLight.addEventListener('change', this.onChange); + this.preferHighContrast.addEventListener('change', this.onChange); } this.dispatcherRef = dis.register(this.onAction); } @@ -62,6 +65,7 @@ export default class ThemeWatcher { if (this.preferDark.addEventListener) { this.preferDark.removeEventListener('change', this.onChange); this.preferLight.removeEventListener('change', this.onChange); + this.preferHighContrast.removeEventListener('change', this.onChange); } SettingsStore.unwatchSetting(this.systemThemeWatchRef); SettingsStore.unwatchSetting(this.themeWatchRef); @@ -108,8 +112,10 @@ export default class ThemeWatcher { SettingLevel.DEVICE, "use_system_theme", null, false, true); if (systemThemeExplicit) { logger.log("returning explicit system theme"); - if (this.preferDark.matches) return 'dark'; - if (this.preferLight.matches) return 'light'; + const theme = this.themeBasedOnSystem(); + if (theme) { + return theme; + } } // If the user has specifically enabled the theme (without the system matching option being @@ -125,13 +131,31 @@ export default class ThemeWatcher { // If the user hasn't really made a preference in either direction, assume the defaults of the // settings and use those. if (SettingsStore.getValue('use_system_theme')) { - if (this.preferDark.matches) return 'dark'; - if (this.preferLight.matches) return 'light'; + const theme = this.themeBasedOnSystem(); + if (theme) { + return theme; + } } logger.log("returning theme value"); return SettingsStore.getValue('theme'); } + private themeBasedOnSystem() { + let newTheme: string; + if (this.preferDark.matches) { + newTheme = 'dark'; + } else if (this.preferLight.matches) { + newTheme = 'light'; + } + if (this.preferHighContrast.matches) { + const hcTheme = findHighContrastTheme(newTheme); + if (hcTheme) { + newTheme = hcTheme; + } + } + return newTheme; + } + public isSystemThemeSupported() { return this.preferDark.matches || this.preferLight.matches; } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index b39e79d36e..848413bccc 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -135,7 +135,7 @@ export class ElementWidget extends Widget { }; } - public getCompleteUrl(params: ITemplateParams, asPopout=false): string { + public getCompleteUrl(params: ITemplateParams, asPopout = false): string { return runTemplate(asPopout ? this.popoutTemplateUrl : this.templateUrl, { ...this.rawDefinition, data: this.rawData, @@ -149,7 +149,7 @@ export class StopGapWidget extends EventEmitter { private scalarToken: string; private roomId?: string; private kind: WidgetKind; - private readUpToMap: {[roomId: string]: string} = {}; // room ID to event ID + private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID constructor(private appTileProps: IAppTileProps) { super(); @@ -262,6 +262,7 @@ export class StopGapWidget extends EventEmitter { this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging.on("preparing", () => this.emit("preparing")); this.messaging.on("ready", () => this.emit("ready")); + this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index f8e720ac19..d0f82227ab 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -73,7 +73,9 @@ export class StopGapWidgetDriver extends WidgetDriver { // Always allow screenshots to be taken because it's a client-induced flow. The widget can't // spew screenshots at us and can't request screenshots of us, so it's up to us to provide the // button if the widget says it supports screenshots. - this.allowedCapabilities = new Set([...allowedCapabilities, MatrixCapabilities.Screenshots]); + this.allowedCapabilities = new Set([...allowedCapabilities, + MatrixCapabilities.Screenshots, + MatrixCapabilities.RequiresClient]); // Grant the permissions that are specific to given widget types if (WidgetType.JITSI.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) { diff --git a/yarn.lock b/yarn.lock index 76da4be19d..df90689cd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5975,10 +5975,10 @@ matrix-react-test-utils@^0.2.3: "@babel/traverse" "^7.13.17" walk "^2.3.14" -matrix-widget-api@^0.1.0-beta.16: - version "0.1.0-beta.16" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.16.tgz#32655f05cab48239b97fe4111a1d0858f2aad61a" - integrity sha512-9zqaNLaM14YDHfFb7WGSUOivGOjYw+w5Su84ZfOl6A4IUy1xT9QPp0nsSA8wNfz0LpxOIPn3nuoF8Tn/40F5tg== +matrix-widget-api@^0.1.0-beta.17: + version "0.1.0-beta.17" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.17.tgz#392be2bf42990e8f7e16aeadf2546f18681af49b" + integrity sha512-hyaDLQNvGvV67Ss23vI69y/ZwVMVz2160LJ2nYyhO0C4mk9zTl0Rbe9jNQ9B453V8MadHLiUUdjzoe++WW+6jA== dependencies: "@types/events" "^3.0.0" events "^3.2.0"