mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Merge branch 'develop' into sort-imports
This commit is contained in:
commit
bc1dd6fedf
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
38
src/@types/browser-encrypt-attachment.ts
Normal file
38
src/@types/browser-encrypt-attachment.ts
Normal 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>;
|
||||
}
|
26
src/@types/png-chunks-extract.ts
Normal file
26
src/@types/png-chunks-extract.ts
Normal 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;
|
||||
}
|
@ -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<HTMLVideoElement> {
|
||||
function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||
const thumbnailType = "image/jpeg";
|
||||
|
||||
let videoInfo;
|
||||
let videoInfo: Partial<IMediaEventInfo>;
|
||||
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<string>;
|
||||
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<void>((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<void>;
|
||||
|
@ -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<DOMRect, "width" | "left" | "top" | "height">;
|
||||
interface IState {
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
isDarkTheme: boolean;
|
||||
isHighContrast: boolean;
|
||||
selectedSpace?: Room;
|
||||
pendingRoomJoin: Set<string>;
|
||||
}
|
||||
@ -88,6 +89,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
this.state = {
|
||||
contextMenuPosition: null,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
isHighContrast: this.isUserOnHighContrastTheme(),
|
||||
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 () => {
|
||||
// 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<IProps, IState> {
|
||||
};
|
||||
|
||||
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<IProps, IState> {
|
||||
// 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
|
||||
};
|
||||
|
||||
|
@ -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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
}
|
||||
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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
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<IProps, IState> {
|
||||
{ this.props.showTitle && this.getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ this.props.showPopout && <AccessibleButton
|
||||
{ (this.props.showPopout && !this.state.requiresClient) && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||
title={_t('Popout widget')}
|
||||
onClick={this.onPopoutWidgetClick}
|
||||
|
@ -62,6 +62,7 @@ import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
||||
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
|
||||
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
@ -993,6 +994,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
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<IProps, IState> {
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
@ -1256,7 +1263,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
{ replyChain }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
@ -1280,7 +1287,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
@ -1340,7 +1347,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
{ avatar }
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ replyChain }
|
||||
|
@ -18,7 +18,6 @@
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
key: {
|
||||
alg: string;
|
||||
key_ops: string[]; // eslint-disable-line camelcase
|
||||
|
@ -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 = (<any>global).matchMedia("(prefers-color-scheme: dark)");
|
||||
this.preferLight = (<any>global).matchMedia("(prefers-color-scheme: light)");
|
||||
this.preferHighContrast = (<any>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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user