Merge branch 'develop' into sort-imports

This commit is contained in:
Aaron Raimist 2021-10-28 19:44:21 -05:00
commit bc1dd6fedf
14 changed files with 246 additions and 63 deletions

View file

@ -85,7 +85,7 @@
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "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", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",

View file

@ -92,6 +92,10 @@ limitations under the License.
&[data-self=false] { &[data-self=false] {
.mx_EventTile_line { .mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius); border-bottom-right-radius: var(--cornerRadius);
.mx_MImageBody .mx_MImageBody_thumbnail {
border-bottom-right-radius: var(--cornerRadius);
}
} }
.mx_EventTile_avatar { .mx_EventTile_avatar {
left: -34px; left: -34px;
@ -106,12 +110,16 @@ limitations under the License.
} }
&[data-self=true] { &[data-self=true] {
.mx_EventTile_line { .mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius);
float: right; float: right;
border-bottom-left-radius: var(--cornerRadius);
> a { > a {
left: auto; left: auto;
right: -68px; right: -68px;
} }
.mx_MImageBody .mx_MImageBody_thumbnail {
border-bottom-left-radius: var(--cornerRadius);
}
} }
.mx_ThreadInfo { .mx_ThreadInfo {
@ -147,33 +155,62 @@ limitations under the License.
.mx_EventTile_line { .mx_EventTile_line {
position: relative; position: relative;
padding: var(--gutterSize);
border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius);
background: var(--backgroundColor);
display: flex; display: flex;
gap: 5px; gap: 5px;
margin: 0 -12px 0 -9px; margin: 0 -12px 0 -9px;
border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius);
> a { > a {
position: absolute; position: absolute;
padding: 10px 20px; padding: 10px 20px;
top: 0; top: 0;
left: -68px; 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 { &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
border-top-left-radius: 0; border-top-left-radius: 0;
.mx_MImageBody .mx_MImageBody_thumbnail {
border-top-left-radius: 0;
}
} }
&.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line { &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius); 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 { &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
border-top-right-radius: 0; border-top-right-radius: 0;
.mx_MImageBody .mx_MImageBody_thumbnail {
border-top-right-radius: 0;
}
} }
&.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line { &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius); border-bottom-right-radius: var(--cornerRadius);
.mx_MImageBody .mx_MImageBody_thumbnail {
border-bottom-right-radius: var(--cornerRadius);
}
} }
.mx_EventTile_avatar { .mx_EventTile_avatar {

View file

@ -2,6 +2,7 @@
$accent: #268075; $accent: #268075;
$alert: #D62C25; $alert: #D62C25;
$links: #0A6ECA; $links: #0A6ECA;
$primary-content: #17191C;
$secondary-content: #5E6266; $secondary-content: #5E6266;
$tertiary-content: $secondary-content; $tertiary-content: $secondary-content;
$quaternary-content: $secondary-content; $quaternary-content: $secondary-content;
@ -106,3 +107,11 @@ $roomtopic-color: $secondary-content;
.mx_FontScalingPanel_fontSlider { .mx_FontScalingPanel_fontSlider {
background-color: $roomlist-button-bg-color !important; 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;
}

View file

@ -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<IEncryptedAttachment>;
export function decryptAttachment(ciphertextBuffer: ArrayBuffer, info: IInfo): Promise<ArrayBuffer>;
}

View file

@ -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;
}

View file

@ -18,16 +18,17 @@ limitations under the License.
import React from "react"; import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import encrypt from "browser-encrypt-attachment"; import { IEncryptedFile, IMediaEventInfo } from "./customisations/models/IMediaEventContent";
import extractPngChunks from "png-chunks-extract"; import { IUploadOpts } from "matrix-js-sdk/src/@types/requests";
import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; import { MsgType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger";
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import * as sdk from './index'; import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import RoomViewStore from './stores/RoomViewStore'; 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 Spinner from "./components/views/elements/Spinner";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
@ -39,10 +40,13 @@ import {
UploadStartedPayload, UploadStartedPayload,
} from "./dispatcher/payloads/UploadPayload"; } from "./dispatcher/payloads/UploadPayload";
import { IUpload } from "./models/IUpload"; import { IUpload } from "./models/IUpload";
import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials";
import { BlurhashEncoder } from "./BlurhashEncoder"; import { BlurhashEncoder } from "./BlurhashEncoder";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics"; import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
import { logger } from "matrix-js-sdk/src/logger";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -306,7 +310,7 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
function infoForVideoFile(matrixClient, roomId, videoFile) { function infoForVideoFile(matrixClient, roomId, videoFile) {
const thumbnailType = "image/jpeg"; const thumbnailType = "image/jpeg";
let videoInfo; let videoInfo: Partial<IMediaEventInfo>;
return loadVideoElement(videoFile).then((video) => { return loadVideoElement(videoFile).then((video) => {
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
}).then((result) => { }).then((result) => {
@ -355,49 +359,48 @@ export function uploadFile(
matrixClient: MatrixClient, matrixClient: MatrixClient,
roomId: string, roomId: string,
file: File | Blob, file: File | Blob,
progressHandler?: any, // TODO: Types progressHandler?: IUploadOpts["progressHandler"],
): IAbortablePromise<{url?: string, file?: any}> { // TODO: Types ): IAbortablePromise<{ url?: string, file?: IEncryptedFile }> {
let canceled = false; let canceled = false;
if (matrixClient.isRoomEncrypted(roomId)) { if (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it. // If the room is encrypted then encrypt the file before uploading it.
// First read the file into memory. // First read the file into memory.
let uploadPromise; let uploadPromise: IAbortablePromise<string>;
let encryptInfo;
const prom = readFileAsArrayBuffer(file).then(function(data) { const prom = readFileAsArrayBuffer(file).then(function(data) {
if (canceled) throw new UploadCanceledError(); if (canceled) throw new UploadCanceledError();
// Then encrypt the file. // Then encrypt the file.
return encrypt.encryptAttachment(data); return encrypt.encryptAttachment(data);
}).then(function(encryptResult) { }).then(function(encryptResult) {
if (canceled) throw new UploadCanceledError(); 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. // Pass the encrypted data as a Blob to the uploader.
const blob = new Blob([encryptResult.data]); const blob = new Blob([encryptResult.data]);
uploadPromise = matrixClient.uploadContent(blob, { uploadPromise = matrixClient.uploadContent(blob, {
progressHandler: progressHandler, progressHandler,
includeFilename: false, includeFilename: false,
}); });
return uploadPromise;
}).then(function(url) { return uploadPromise.then(url => {
if (canceled) throw new UploadCanceledError(); if (canceled) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and // If the attachment is encrypted then bundle the URL along
// add it under a file key. // with the information needed to decrypt the attachment and
encryptInfo.url = url; // add it under a file key.
if (file.type) { return {
encryptInfo.mimetype = file.type; file: {
} ...encryptResult.info,
return { "file": encryptInfo }; url,
}) as IAbortablePromise<{ file: any }>; },
};
});
}) as IAbortablePromise<{ file: IEncryptedFile }>;
prom.abort = () => { prom.abort = () => {
canceled = true; canceled = true;
if (uploadPromise) matrixClient.cancelUpload(uploadPromise); if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
}; };
return prom; return prom;
} else { } else {
const basePromise = matrixClient.uploadContent(file, { const basePromise = matrixClient.uploadContent(file, { progressHandler });
progressHandler: progressHandler,
});
const promise1 = basePromise.then(function(url) { const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError(); if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly. // If the attachment isn't encrypted then include the URL directly.
@ -553,29 +556,29 @@ export default class ContentMessages {
const prom = new Promise<void>((resolve) => { const prom = new Promise<void>((resolve) => {
if (file.type.indexOf('image/') === 0) { if (file.type.indexOf('image/') === 0) {
content.msgtype = 'm.image'; content.msgtype = MsgType.Image;
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
Object.assign(content.info, imageInfo); Object.assign(content.info, imageInfo);
resolve(); resolve();
}, (e) => { }, (e) => {
logger.error(e); logger.error(e);
content.msgtype = 'm.file'; content.msgtype = MsgType.File;
resolve(); resolve();
}); });
} else if (file.type.indexOf('audio/') === 0) { } else if (file.type.indexOf('audio/') === 0) {
content.msgtype = 'm.audio'; content.msgtype = MsgType.Audio;
resolve(); resolve();
} else if (file.type.indexOf('video/') === 0) { } else if (file.type.indexOf('video/') === 0) {
content.msgtype = 'm.video'; content.msgtype = MsgType.Video;
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
Object.assign(content.info, videoInfo); Object.assign(content.info, videoInfo);
resolve(); resolve();
}, (e) => { }, (e) => {
content.msgtype = 'm.file'; content.msgtype = MsgType.File;
resolve(); resolve();
}); });
} else { } else {
content.msgtype = 'm.file'; content.msgtype = MsgType.File;
resolve(); resolve();
} }
}) as IAbortablePromise<void>; }) as IAbortablePromise<void>;

View file

@ -33,7 +33,7 @@ import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal"; import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog"; import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { getCustomTheme } from "../../theme"; import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { getHomePageUrl } from "../../utils/pages"; import { getHomePageUrl } from "../../utils/pages";
@ -70,6 +70,7 @@ type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
interface IState { interface IState {
contextMenuPosition: PartialDOMRect; contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean; isDarkTheme: boolean;
isHighContrast: boolean;
selectedSpace?: Room; selectedSpace?: Room;
pendingRoomJoin: Set<string>; pendingRoomJoin: Set<string>;
} }
@ -88,6 +89,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.state = { this.state = {
contextMenuPosition: null, contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(), isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(),
pendingRoomJoin: new Set<string>(), pendingRoomJoin: new Set<string>(),
}; };
@ -143,6 +145,18 @@ export default class UserMenu extends React.Component<IProps, IState> {
} }
} }
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 () => { private onProfileUpdate = async () => {
// the store triggered an update, so force a layout update. We don't // the store triggered an update, so force a layout update. We don't
// have any state to store here for that to magically happen. // have any state to store here for that to magically happen.
@ -154,7 +168,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
}; };
private onThemeChanged = () => { private onThemeChanged = () => {
this.setState({ isDarkTheme: this.isUserOnDarkTheme() }); this.setState(
{
isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(),
});
}; };
private onAction = (ev: ActionPayload) => { private onAction = (ev: ActionPayload) => {
@ -222,7 +240,13 @@ export default class UserMenu extends React.Component<IProps, IState> {
// Disable system theme matching if the user hits this button // Disable system theme matching if the user hits this button
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); 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 SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
}; };

View file

@ -86,6 +86,7 @@ interface IState {
error: Error; error: Error;
menuDisplayed: boolean; menuDisplayed: boolean;
widgetPageTitle: string; widgetPageTitle: string;
requiresClient: boolean;
} }
@replaceableComponent("views.elements.AppTile") @replaceableComponent("views.elements.AppTile")
@ -114,8 +115,10 @@ export default class AppTile extends React.Component<IProps, IState> {
this.persistKey = getPersistKey(this.props.app.id); this.persistKey = getPersistKey(this.props.app.id);
try { try {
this.sgWidget = new StopGapWidget(this.props); this.sgWidget = new StopGapWidget(this.props);
this.sgWidget.on("preparing", this.onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPreparing);
this.sgWidget.on("ready", this.onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
// emits when the capabilites have been setup or changed
this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
} catch (e) { } catch (e) {
logger.log("Failed to construct widget", e); logger.log("Failed to construct widget", e);
this.sgWidget = null; this.sgWidget = null;
@ -155,6 +158,10 @@ export default class AppTile extends React.Component<IProps, IState> {
error: null, error: null,
menuDisplayed: false, menuDisplayed: false,
widgetPageTitle: this.props.widgetPageTitle, 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<IProps, IState> {
} }
try { try {
this.sgWidget = new StopGapWidget(newProps); this.sgWidget = new StopGapWidget(newProps);
this.sgWidget.on("preparing", this.onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPreparing);
this.sgWidget.on("ready", this.onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
this.startWidget(); this.startWidget();
} catch (e) { } catch (e) {
@ -287,7 +294,7 @@ export default class AppTile extends React.Component<IProps, IState> {
if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true }); if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
} }
private onWidgetPrepared = (): void => { private onWidgetPreparing = (): void => {
this.setState({ loading: false }); this.setState({ loading: false });
}; };
@ -297,6 +304,12 @@ export default class AppTile extends React.Component<IProps, IState> {
} }
}; };
private onWidgetCapabilitiesNotified = (): void => {
this.setState({
requiresClient: this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.RequiresClient),
});
};
private onAction = (payload): void => { private onAction = (payload): void => {
if (payload.widgetId === this.props.app.id) { if (payload.widgetId === this.props.app.id) {
switch (payload.action) { switch (payload.action) {
@ -512,7 +525,7 @@ export default class AppTile extends React.Component<IProps, IState> {
{ this.props.showTitle && this.getTileTitle() } { this.props.showTitle && this.getTileTitle() }
</span> </span>
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ this.props.showPopout && <AccessibleButton { (this.props.showPopout && !this.state.requiresClient) && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')} title={_t('Popout widget')}
onClick={this.onPopoutWidgetClick} onClick={this.onPopoutWidgetClick}

View file

@ -62,6 +62,7 @@ import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads'; import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore'; import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
import { TimelineRenderingType } from "../../../contexts/RoomContext"; import { TimelineRenderingType } from "../../../contexts/RoomContext";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
const eventTileTypes = { const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent', [EventType.RoomMessage]: 'messages.MessageEvent',
@ -993,6 +994,12 @@ export default class EventTile extends React.Component<IProps, IState> {
} }
const EventTileType = sdk.getComponent(tileHandler); 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 isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
@ -1208,7 +1215,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ timestamp } { timestamp }
</a> </a>
</div>, </div>,
<div className="mx_EventTile_line" key="mx_EventTile_line"> <div className={lineClasses} key="mx_EventTile_line">
<EventTileType ref={this.tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
@ -1256,7 +1263,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ timestamp } { timestamp }
</a> </a>
</div>, </div>,
<div className="mx_EventTile_line" key="mx_EventTile_line"> <div className={lineClasses} key="mx_EventTile_line">
{ replyChain } { replyChain }
<EventTileType ref={this.tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
@ -1280,7 +1287,7 @@ export default class EventTile extends React.Component<IProps, IState> {
"aria-atomic": true, "aria-atomic": true,
"data-scroll-tokens": scrollToken, "data-scroll-tokens": scrollToken,
}, [ }, [
<div className="mx_EventTile_line" key="mx_EventTile_line"> <div className={lineClasses} key="mx_EventTile_line">
<EventTileType ref={this.tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
@ -1340,7 +1347,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ sender } { sender }
{ ircPadlock } { ircPadlock }
{ avatar } { avatar }
<div className="mx_EventTile_line" key="mx_EventTile_line"> <div className={lineClasses} key="mx_EventTile_line">
{ groupTimestamp } { groupTimestamp }
{ groupPadlock } { groupPadlock }
{ replyChain } { replyChain }

View file

@ -18,7 +18,6 @@
export interface IEncryptedFile { export interface IEncryptedFile {
url: string; url: string;
mimetype?: string;
key: { key: {
alg: string; alg: string;
key_ops: string[]; // eslint-disable-line camelcase key_ops: string[]; // eslint-disable-line camelcase

View file

@ -21,7 +21,7 @@ import SettingsStore from '../SettingsStore';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import { Action } from '../../dispatcher/actions'; import { Action } from '../../dispatcher/actions';
import ThemeController from "../controllers/ThemeController"; import ThemeController from "../controllers/ThemeController";
import { setTheme } from "../../theme"; import { findHighContrastTheme, setTheme } from "../../theme";
import { ActionPayload } from '../../dispatcher/payloads'; import { ActionPayload } from '../../dispatcher/payloads';
import { SettingLevel } from "../SettingLevel"; import { SettingLevel } from "../SettingLevel";
@ -32,6 +32,7 @@ export default class ThemeWatcher {
private preferDark: MediaQueryList; private preferDark: MediaQueryList;
private preferLight: MediaQueryList; private preferLight: MediaQueryList;
private preferHighContrast: MediaQueryList;
private currentTheme: string; private currentTheme: string;
@ -44,6 +45,7 @@ export default class ThemeWatcher {
// we can get the tristate of dark/light/unsupported // we can get the tristate of dark/light/unsupported
this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)"); this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)");
this.preferLight = (<any>global).matchMedia("(prefers-color-scheme: light)"); this.preferLight = (<any>global).matchMedia("(prefers-color-scheme: light)");
this.preferHighContrast = (<any>global).matchMedia("(prefers-contrast: more)");
this.currentTheme = this.getEffectiveTheme(); this.currentTheme = this.getEffectiveTheme();
} }
@ -54,6 +56,7 @@ export default class ThemeWatcher {
if (this.preferDark.addEventListener) { if (this.preferDark.addEventListener) {
this.preferDark.addEventListener('change', this.onChange); this.preferDark.addEventListener('change', this.onChange);
this.preferLight.addEventListener('change', this.onChange); this.preferLight.addEventListener('change', this.onChange);
this.preferHighContrast.addEventListener('change', this.onChange);
} }
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
} }
@ -62,6 +65,7 @@ export default class ThemeWatcher {
if (this.preferDark.addEventListener) { if (this.preferDark.addEventListener) {
this.preferDark.removeEventListener('change', this.onChange); this.preferDark.removeEventListener('change', this.onChange);
this.preferLight.removeEventListener('change', this.onChange); this.preferLight.removeEventListener('change', this.onChange);
this.preferHighContrast.removeEventListener('change', this.onChange);
} }
SettingsStore.unwatchSetting(this.systemThemeWatchRef); SettingsStore.unwatchSetting(this.systemThemeWatchRef);
SettingsStore.unwatchSetting(this.themeWatchRef); SettingsStore.unwatchSetting(this.themeWatchRef);
@ -108,8 +112,10 @@ export default class ThemeWatcher {
SettingLevel.DEVICE, "use_system_theme", null, false, true); SettingLevel.DEVICE, "use_system_theme", null, false, true);
if (systemThemeExplicit) { if (systemThemeExplicit) {
logger.log("returning explicit system theme"); logger.log("returning explicit system theme");
if (this.preferDark.matches) return 'dark'; const theme = this.themeBasedOnSystem();
if (this.preferLight.matches) return 'light'; if (theme) {
return theme;
}
} }
// If the user has specifically enabled the theme (without the system matching option being // 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 // If the user hasn't really made a preference in either direction, assume the defaults of the
// settings and use those. // settings and use those.
if (SettingsStore.getValue('use_system_theme')) { if (SettingsStore.getValue('use_system_theme')) {
if (this.preferDark.matches) return 'dark'; const theme = this.themeBasedOnSystem();
if (this.preferLight.matches) return 'light'; if (theme) {
return theme;
}
} }
logger.log("returning theme value"); logger.log("returning theme value");
return SettingsStore.getValue('theme'); 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() { public isSystemThemeSupported() {
return this.preferDark.matches || this.preferLight.matches; return this.preferDark.matches || this.preferLight.matches;
} }

View file

@ -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, { return runTemplate(asPopout ? this.popoutTemplateUrl : this.templateUrl, {
...this.rawDefinition, ...this.rawDefinition,
data: this.rawData, data: this.rawData,
@ -149,7 +149,7 @@ export class StopGapWidget extends EventEmitter {
private scalarToken: string; private scalarToken: string;
private roomId?: string; private roomId?: string;
private kind: WidgetKind; 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) { constructor(private appTileProps: IAppTileProps) {
super(); super();
@ -262,6 +262,7 @@ export class StopGapWidget extends EventEmitter {
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
this.messaging.on("preparing", () => this.emit("preparing")); this.messaging.on("preparing", () => this.emit("preparing"));
this.messaging.on("ready", () => this.emit("ready")); this.messaging.on("ready", () => this.emit("ready"));
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);

View file

@ -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 // 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 // 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. // 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 // Grant the permissions that are specific to given widget types
if (WidgetType.JITSI.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) { if (WidgetType.JITSI.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) {

View file

@ -5975,10 +5975,10 @@ matrix-react-test-utils@^0.2.3:
"@babel/traverse" "^7.13.17" "@babel/traverse" "^7.13.17"
walk "^2.3.14" walk "^2.3.14"
matrix-widget-api@^0.1.0-beta.16: matrix-widget-api@^0.1.0-beta.17:
version "0.1.0-beta.16" version "0.1.0-beta.17"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.16.tgz#32655f05cab48239b97fe4111a1d0858f2aad61a" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.17.tgz#392be2bf42990e8f7e16aeadf2546f18681af49b"
integrity sha512-9zqaNLaM14YDHfFb7WGSUOivGOjYw+w5Su84ZfOl6A4IUy1xT9QPp0nsSA8wNfz0LpxOIPn3nuoF8Tn/40F5tg== integrity sha512-hyaDLQNvGvV67Ss23vI69y/ZwVMVz2160LJ2nYyhO0C4mk9zTl0Rbe9jNQ9B453V8MadHLiUUdjzoe++WW+6jA==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"