Merge branch 'develop' into t3chguy/ts/11

This commit is contained in:
Michael Telatynski
2021-07-02 11:03:54 +01:00
committed by GitHub
61 changed files with 467 additions and 766 deletions
+3 -3
View File
@@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields {
}
interface ThirdpartyLookupResponse {
userid: string,
protocol: string,
fields: ThirdpartyLookupResponseFields,
userid: string;
protocol: string;
fields: ThirdpartyLookupResponseFields;
}
// Unlike 'CallType' in js-sdk, this one includes screen sharing
+40 -18
View File
@@ -17,9 +17,10 @@ limitations under the License.
*/
import React from "react";
import dis from './dispatcher/dispatcher';
import { MatrixClientPeg } from './MatrixClientPeg';
import { encode } from "blurhash";
import { MatrixClient } from "matrix-js-sdk/src/client";
import dis from './dispatcher/dispatcher';
import * as sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
@@ -47,6 +48,10 @@ const MAX_HEIGHT = 600;
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
const BLURHASH_X_COMPONENTS = 6;
const BLURHASH_Y_COMPONENTS = 6;
export class UploadCanceledError extends Error {}
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
@@ -77,6 +82,7 @@ interface IThumbnail {
};
w: number;
h: number;
[BLURHASH_FIELD]: string;
};
thumbnail: Blob;
}
@@ -124,7 +130,16 @@ function createThumbnail(
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
const context = canvas.getContext("2d");
context.drawImage(element, 0, 0, targetWidth, targetHeight);
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
const blurhash = encode(
imageData.data,
imageData.width,
imageData.height,
BLURHASH_X_COMPONENTS,
BLURHASH_Y_COMPONENTS,
);
canvas.toBlob(function(thumbnail) {
resolve({
info: {
@@ -136,8 +151,9 @@ function createThumbnail(
},
w: inputWidth,
h: inputHeight,
[BLURHASH_FIELD]: blurhash,
},
thumbnail: thumbnail,
thumbnail,
});
}, mimeType);
});
@@ -220,7 +236,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
}
/**
* Load a file into a newly created video element.
* Load a file into a newly created video element and pull some strings
* in an attempt to guarantee the first frame will be showing.
*
* @param {File} videoFile The file to load in an video element.
* @return {Promise} A promise that resolves with the video image element.
@@ -229,20 +246,25 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
return new Promise((resolve, reject) => {
// Load the file into an html element
const video = document.createElement("video");
video.preload = "metadata";
video.playsInline = true;
video.muted = true;
const reader = new FileReader();
reader.onload = function(ev) {
video.src = ev.target.result as string;
// Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = function() {
video.onloadeddata = async function() {
resolve(video);
video.pause();
};
video.onerror = function(e) {
reject(e);
};
video.src = ev.target.result as string;
video.load();
video.play();
};
reader.onerror = function(e) {
reject(e);
@@ -347,7 +369,7 @@ export function uploadFile(
});
(prom as IAbortablePromise<any>).abort = () => {
canceled = true;
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
};
return prom;
} else {
@@ -357,11 +379,11 @@ export function uploadFile(
const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
return { "url": url };
return { url };
});
(promise1 as any).abort = () => {
canceled = true;
MatrixClientPeg.get().cancelUpload(basePromise);
matrixClient.cancelUpload(basePromise);
};
return promise1;
}
@@ -373,7 +395,7 @@ export default class ContentMessages {
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
const startTime = CountlyAnalytics.getTimestamp();
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => {
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
throw e;
});
@@ -415,7 +437,7 @@ export default class ContentMessages {
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
await this.ensureMediaConfigFetched();
await this.ensureMediaConfigFetched(matrixClient);
modal.close();
}
@@ -470,7 +492,7 @@ export default class ContentMessages {
return this.inprogress.filter(u => !u.canceled);
}
cancelUpload(promise: Promise<any>) {
cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
let upload: IUpload;
for (let i = 0; i < this.inprogress.length; ++i) {
if (this.inprogress[i].promise === promise) {
@@ -480,7 +502,7 @@ export default class ContentMessages {
}
if (upload) {
upload.canceled = true;
MatrixClientPeg.get().cancelUpload(upload.promise);
matrixClient.cancelUpload(upload.promise);
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
}
}
@@ -621,11 +643,11 @@ export default class ContentMessages {
return true;
}
private ensureMediaConfigFetched() {
private ensureMediaConfigFetched(matrixClient: MatrixClient) {
if (this.mediaConfig !== null) return;
console.log("[Media Config] Fetching");
return MatrixClientPeg.get().getMediaConfig().then((config) => {
return matrixClient.getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
+1 -1
View File
@@ -256,7 +256,7 @@ interface ICreateRoomEvent extends IEvent {
num_users: number;
is_encrypted: boolean;
is_public: boolean;
}
};
}
interface IJoinRoomEvent extends IEvent {
+44 -35
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@@ -15,16 +16,24 @@ limitations under the License.
*/
import * as commonmark from 'commonmark';
import {escape} from "lodash";
import { escape } from "lodash";
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
// These types of node are definitely text
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
function is_allowed_html_tag(node) {
// As far as @types/commonmark is concerned, these are not public, so add them
interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer {
paragraph: (node: commonmark.Node, entering: boolean) => void;
link: (node: commonmark.Node, entering: boolean) => void;
html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase
html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase
}
function isAllowedHtmlTag(node: commonmark.Node): boolean {
if (node.literal != null &&
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
return true;
}
@@ -39,21 +48,12 @@ function is_allowed_html_tag(node) {
return false;
}
function html_if_tag_allowed(node) {
if (is_allowed_html_tag(node)) {
this.lit(node.literal);
return;
} else {
this.lit(escape(node.literal));
}
}
/*
* Returns true if the parse output containing the node
* comprises multiple block level elements (ie. lines),
* or false if it is only a single line.
*/
function is_multi_line(node) {
function isMultiLine(node: commonmark.Node): boolean {
let par = node;
while (par.parent) {
par = par.parent;
@@ -67,6 +67,9 @@ function is_multi_line(node) {
* it's plain text.
*/
export default class Markdown {
private input: string;
private parsed: commonmark.Node;
constructor(input) {
this.input = input;
@@ -74,7 +77,7 @@ export default class Markdown {
this.parsed = parser.parse(this.input);
}
isPlainText() {
isPlainText(): boolean {
const walker = this.parsed.walker();
let ev;
@@ -87,7 +90,7 @@ export default class Markdown {
// if it's an allowed html tag, we need to render it and therefore
// we will need to use HTML. If it's not allowed, it's not HTML since
// we'll just be treating it as text.
if (is_allowed_html_tag(node)) {
if (isAllowedHtmlTag(node)) {
return false;
}
} else {
@@ -97,7 +100,7 @@ export default class Markdown {
return true;
}
toHTML({ externalLinks = false } = {}) {
toHTML({ externalLinks = false } = {}): string {
const renderer = new commonmark.HtmlRenderer({
safe: false,
@@ -107,7 +110,7 @@ export default class Markdown {
// block quote ends up all on one line
// (https://github.com/vector-im/element-web/issues/3154)
softbreak: '<br />',
});
}) as CommonmarkHtmlRendererInternal;
// Trying to strip out the wrapping <p/> causes a lot more complication
// than it's worth, i think. For instance, this code will go and strip
@@ -118,16 +121,16 @@ export default class Markdown {
//
// Let's try sending with <p/>s anyway for now, though.
const real_paragraph = renderer.paragraph;
const realParagraph = renderer.paragraph;
renderer.paragraph = function(node, entering) {
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
// If there is only one top level node, just return the
// bare text: it's a single line of text and so should be
// 'inline', rather than unnecessarily wrapped in its own
// p tag. If, however, we have multiple nodes, each gets
// its own p tag to keep them as separate paragraphs.
if (is_multi_line(node)) {
real_paragraph.call(this, node, entering);
if (isMultiLine(node)) {
realParagraph.call(this, node, entering);
}
};
@@ -150,19 +153,26 @@ export default class Markdown {
}
};
renderer.html_inline = html_if_tag_allowed;
renderer.html_inline = function(node: commonmark.Node) {
if (isAllowedHtmlTag(node)) {
this.lit(node.literal);
return;
} else {
this.lit(escape(node.literal));
}
};
renderer.html_block = function(node) {
/*
renderer.html_block = function(node: commonmark.Node) {
/*
// as with `paragraph`, we only insert line breaks
// if there are multiple lines in the markdown.
const isMultiLine = is_multi_line(node);
if (isMultiLine) this.cr();
*/
html_if_tag_allowed.call(this, node);
/*
*/
renderer.html_inline(node);
/*
if (isMultiLine) this.cr();
*/
*/
};
return renderer.render(this.parsed);
@@ -177,23 +187,22 @@ export default class Markdown {
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
* which has no formatting. Otherwise it emits HTML(!).
*/
toPlaintext() {
const renderer = new commonmark.HtmlRenderer({safe: false});
const real_paragraph = renderer.paragraph;
toPlaintext(): string {
const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
renderer.paragraph = function(node, entering) {
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
// as with toHTML, only append lines to paragraphs if there are
// multiple paragraphs
if (is_multi_line(node)) {
if (isMultiLine(node)) {
if (!entering && node.next) {
this.lit('\n\n');
}
}
};
renderer.html_block = function(node) {
renderer.html_block = function(node: commonmark.Node) {
this.lit(node.literal);
if (is_multi_line(node) && node.next) this.lit('\n\n');
if (isMultiLine(node) && node.next) this.lit('\n\n');
};
return renderer.render(this.parsed);
+2 -2
View File
@@ -42,8 +42,8 @@ let secretStorageBeingAccessed = false;
let nonInteractive = false;
let dehydrationCache: {
key?: Uint8Array,
keyInfo?: ISecretStorageKeyInfo,
key?: Uint8Array;
keyInfo?: ISecretStorageKeyInfo;
} = {};
function isCachingAllowed(): boolean {
+5 -5
View File
@@ -49,13 +49,13 @@ export interface Policy {
}
export type Policies = {
[policy: string]: Policy,
[policy: string]: Policy;
};
export type TermsInteractionCallback = (
policiesAndServicePairs: {
service: Service,
policies: Policies,
service: Service;
policies: Policies;
}[],
agreedUrls: string[],
extraClassNames?: string,
@@ -181,8 +181,8 @@ export async function startTermsFlow(
export function dialogTermsInteractionCallback(
policiesAndServicePairs: {
service: Service,
policies: { [policy: string]: Policy },
service: Service;
policies: { [policy: string]: Policy };
}[],
agreedUrls: string[],
extraClassNames?: string,
@@ -21,8 +21,8 @@ interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
className?: string;
onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties
tabIndex?: number,
style?: React.CSSProperties;
tabIndex?: number;
wrappedRef?: (ref: HTMLDivElement) => void;
}
+1 -1
View File
@@ -38,7 +38,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
interface IProps {
roomId: string;
onClose: () => void;
resizeNotifier: ResizeNotifier
resizeNotifier: ResizeNotifier;
}
interface IState {
+4 -4
View File
@@ -48,7 +48,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
@@ -81,14 +81,14 @@ interface IProps {
page_type: string;
autoJoin: boolean;
threepidInvite?: IThreepidInvite;
roomOobData?: object;
roomOobData?: IOOBData;
currentRoomId: string;
collapseLhs: boolean;
config: {
piwik: {
policyUrl: string;
},
[key: string]: any,
};
[key: string]: any;
};
currentUserId?: string;
currentGroupId?: string;
+1 -1
View File
@@ -204,7 +204,7 @@ interface IState {
resizeNotifier: ResizeNotifier;
serverConfig?: ValidatedServerConfig;
ready: boolean;
threepidInvite?: IThreepidInvite,
threepidInvite?: IThreepidInvite;
roomOobData?: object;
pendingInitialSync?: boolean;
justRegistered?: boolean;
@@ -24,7 +24,7 @@ interface IProps {
}
interface IState {
toasts: ComponentClass[],
toasts: ComponentClass[];
}
@replaceableComponent("structures.NonUrgentToastContainer")
+2 -2
View File
@@ -370,7 +370,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
private onFilterChange = (alias: string) => {
this.setState({
filterString: alias || null,
filterString: alias || "",
});
// don't send the request for a little bit,
@@ -389,7 +389,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
private onFilterClear = () => {
// update immediately
this.setState({
filterString: null,
filterString: "",
}, this.refreshRoomList);
if (this.filterTimeout) {
+4 -26
View File
@@ -63,7 +63,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import { XOR } from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay";
import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects';
@@ -94,22 +94,8 @@ if (DEBUG) {
}
interface IProps {
threepidInvite: IThreepidInvite,
// Any data about the room that would normally come from the homeserver
// but has been passed out-of-band, eg. the room name and avatar URL
// from an email invite (a workaround for the fact that we can't
// get this information from the HS using an email invite).
// Fields:
// * name (string) The room's name
// * avatarUrl (string) The mxc:// avatar URL for the room
// * inviterName (string) The display name of the person who
// * invited us to the room
oobData?: {
name?: string;
avatarUrl?: string;
inviterName?: string;
};
threepidInvite: IThreepidInvite;
oobData?: IOOBData;
resizeNotifier: ResizeNotifier;
justCreatedOpts?: IOpts;
@@ -1261,7 +1247,7 @@ export default class RoomView extends React.Component<IProps, IState> {
});
};
private injectSticker(url, info, text) {
private injectSticker(url: string, info: object, text: string) {
if (this.context.isGuest()) {
dis.dispatch({ action: 'require_registration' });
return;
@@ -1460,13 +1446,6 @@ export default class RoomView extends React.Component<IProps, IState> {
});
};
private onLeaveClick = () => {
dis.dispatch({
action: 'leave_room',
room_id: this.state.room.roomId,
});
};
private onForgetClick = () => {
dis.dispatch({
action: 'forget_room',
@@ -2106,7 +2085,6 @@ export default class RoomView extends React.Component<IProps, IState> {
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
appsShown={this.state.showApps}
@@ -58,7 +58,7 @@ export interface ISpaceSummaryRoom {
avatar_url?: string;
guest_can_join: boolean;
name?: string;
num_joined_members: number
num_joined_members: number;
room_id: string;
topic?: string;
world_readable: boolean;
+1 -1
View File
@@ -125,7 +125,7 @@ interface IProps {
onReadMarkerUpdated?(): void;
// callback which is called when we wish to paginate the timeline window.
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>,
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>;
}
interface IState {
+4 -1
View File
@@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload";
import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
room: Room;
@@ -38,6 +39,8 @@ interface IState {
@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
private dispatcherRef: string;
private mounted: boolean;
@@ -82,7 +85,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
private onCancelClick = (ev) => {
ev.preventDefault();
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context);
};
render() {
@@ -49,7 +49,7 @@ interface IProps {
// for operations like uploading cross-signing keys).
onLoggedIn(params: {
userId: string;
deviceId: string
deviceId: string;
homeserverUrl: string;
identityServerUrl?: string;
accessToken: string;
@@ -49,7 +49,7 @@ interface IProps {
fragmentAfterLogin?: string;
// Called when the SSO login completes
onTokenLoginCompleted: () => void,
onTokenLoginCompleted: () => void;
}
interface IState {
+2 -2
View File
@@ -52,8 +52,8 @@ interface IProps {
interface IState {
fieldValid: Partial<Record<LoginField, boolean>>;
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone,
password: "",
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone;
password: "";
}
enum LoginField {
@@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
interface IProps {
room: Room;
avatarSize: number;
displayBadge?: boolean;
forceCount?: boolean;
oobData?: object;
oobData?: IOOBData;
viewAvatarOnClick?: boolean;
}
+2 -2
View File
@@ -24,14 +24,14 @@ import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { IOOBData } from '../../../stores/ThreepidInviteStore';
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is,
// oobData.avatarUrl should be set (else there
// would be nowhere to get the avatar from)
room?: Room;
// TODO: type when js-sdk has types
oobData?: any;
oobData?: IOOBData;
width?: number;
height?: number;
resizeMethod?: ResizeMethod;
@@ -29,7 +29,7 @@ interface IProps {
// group member object. Supply either this or 'member'
groupMember: GroupMemberType;
// needed if a group member is specified
matrixClient?: MatrixClient,
matrixClient?: MatrixClient;
action: string; // eg. 'Ban'
title: string; // eg. 'Ban this user?'
@@ -70,9 +70,9 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
/* eslint-disable camelcase */
interface IRecentUser {
userId: string,
user: RoomMember,
lastActive: number,
userId: string;
user: RoomMember;
lastActive: number;
}
export const KIND_DM = "dm";
@@ -330,16 +330,16 @@ interface IInviteDialogProps {
// The kind of invite being performed. Assumed to be KIND_DM if
// not provided.
kind: string,
kind: string;
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: string,
roomId: string;
// The call to transfer. Only required for KIND_CALL_TRANSFER.
call: MatrixCall,
call: MatrixCall;
// Initial value to populate the filter with
initialText: string,
initialText: string;
}
interface IInviteDialogState {
@@ -356,8 +356,8 @@ interface IInviteDialogState {
consultFirst: boolean;
// These two flags are used for the 'Go' button to communicate what is going on.
busy: boolean,
errorText: string,
busy: boolean;
errorText: string;
}
@replaceableComponent("views.dialogs.InviteDialog")
+3 -3
View File
@@ -46,19 +46,19 @@ interface ITermsDialogProps {
* Array of [Service, policies] pairs, where policies is the response from the
* /terms endpoint for that service
*/
policiesAndServicePairs: any[],
policiesAndServicePairs: any[];
/**
* urls that the user has already agreed to
*/
agreedUrls?: string[],
agreedUrls?: string[];
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: (success: boolean, agreedUrls?: string[]) => void,
onFinished: (success: boolean, agreedUrls?: string[]) => void;
}
interface IState {
@@ -0,0 +1,56 @@
/*
Copyright 2020 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 React from 'react';
import { decode } from "blurhash";
interface IProps {
blurhash: string;
width: number;
height: number;
}
export default class BlurhashPlaceholder extends React.PureComponent<IProps> {
private canvas: React.RefObject<HTMLCanvasElement> = React.createRef();
public componentDidMount() {
this.draw();
}
public componentDidUpdate() {
this.draw();
}
private draw() {
if (!this.canvas.current) return;
try {
const { width, height } = this.props;
const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height));
const ctx = this.canvas.current.getContext("2d");
const imgData = ctx.createImageData(width, height);
imgData.data.set(pixels);
ctx.putImageData(imgData, 0, 0);
} catch (e) {
console.error("Error rendering blurhash: ", e);
}
}
public render() {
return <canvas height={this.props.height} width={this.props.width} ref={this.canvas} />;
}
}
@@ -29,7 +29,7 @@ interface IProps {
// The minimum number of events needed to trigger summarisation
threshold?: number;
// Whether or not to begin with state.expanded=true
startExpanded?: boolean,
startExpanded?: boolean;
// The list of room members for which to show avatars next to the summary
summaryMembers?: RoomMember[];
// The text to show as the summary of this event list
+17 -17
View File
@@ -44,31 +44,31 @@ const ZOOM_COEFFICIENT = 0.0025;
const ZOOM_DISTANCE = 10;
interface IProps {
src: string, // the source of the image being displayed
name?: string, // the main title ('name') for the image
link?: string, // the link (if any) applied to the name of the image
width?: number, // width of the image src in pixels
height?: number, // height of the image src in pixels
fileSize?: number, // size of the image src in bytes
onFinished(): void, // callback when the lightbox is dismissed
src: string; // the source of the image being displayed
name?: string; // the main title ('name') for the image
link?: string; // the link (if any) applied to the name of the image
width?: number; // width of the image src in pixels
height?: number; // height of the image src in pixels
fileSize?: number; // size of the image src in bytes
onFinished(): void; // callback when the lightbox is dismissed
// the event (if any) that the Image is displaying. Used for event-specific stuff like
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
// properties above, which let us use lightboxes to display images which aren't associated
// with events.
mxEvent: MatrixEvent,
permalinkCreator: RoomPermalinkCreator,
mxEvent: MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
}
interface IState {
zoom: number,
minZoom: number,
maxZoom: number,
rotation: number,
translationX: number,
translationY: number,
moving: boolean,
contextMenuDisplayed: boolean,
zoom: number;
minZoom: number;
maxZoom: number;
rotation: number;
translationX: number;
translationY: number;
moving: boolean;
contextMenuDisplayed: boolean;
}
@replaceableComponent("views.elements.ImageView")
@@ -30,14 +30,14 @@ function languageMatchesSearchQuery(query, language) {
}
interface SpellCheckLanguagesDropdownIProps {
className: string,
value: string,
onOptionChange(language: string),
className: string;
value: string;
onOptionChange(language: string);
}
interface SpellCheckLanguagesDropdownIState {
searchQuery: string,
languages: any,
searchQuery: string;
languages: any;
}
@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
+1 -1
View File
@@ -25,7 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
categories: ICategory[];
onAnchorClick(id: CategoryKey): void
onAnchorClick(id: CategoryKey): void;
}
@replaceableComponent("views.emojipicker.Header")
+14 -14
View File
@@ -29,6 +29,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import InlineSpinner from '../elements/InlineSpinner';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
import { BLURHASH_FIELD } from "../../../ContentMessages";
@replaceableComponent("views.messages.MImageBody")
export default class MImageBody extends React.Component {
@@ -333,7 +335,8 @@ export default class MImageBody extends React.Component {
infoWidth = content.info.w;
infoHeight = content.info.h;
} else {
// Whilst the image loads, display nothing.
// Whilst the image loads, display nothing. We also don't display a blurhash image
// because we don't really know what size of image we'll end up with.
//
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
//
@@ -368,12 +371,8 @@ export default class MImageBody extends React.Component {
let placeholder = null;
let gifLabel = null;
// e2e image hasn't been decrypted yet
if (content.file !== undefined && this.state.decryptedUrl === null) {
placeholder = <InlineSpinner w={32} h={32} />;
} else if (!this.state.imgLoaded) {
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
placeholder = this.getPlaceholder();
if (!this.state.imgLoaded) {
placeholder = this.getPlaceholder(maxWidth, maxHeight);
}
let showPlaceholder = Boolean(placeholder);
@@ -395,7 +394,7 @@ export default class MImageBody extends React.Component {
if (!this.state.showImage) {
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon.
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
}
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
@@ -411,9 +410,7 @@ export default class MImageBody extends React.Component {
// Constrain width here so that spinner appears central to the loaded thumbnail
maxWidth: infoWidth + "px",
}}>
<div className="mx_MImageBody_thumbnail_spinner">
{ placeholder }
</div>
{ placeholder }
</div>
}
@@ -437,9 +434,12 @@ export default class MImageBody extends React.Component {
}
// Overidden by MStickerBody
getPlaceholder() {
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
return null;
getPlaceholder(width, height) {
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
if (blurhash) return <BlurhashPlaceholder blurhash={blurhash} width={width} height={height} />;
return <div className="mx_MImageBody_thumbnail_spinner">
<InlineSpinner w={32} h={32} />
</div>;
}
// Overidden by MStickerBody
@@ -28,7 +28,7 @@ import EventTileBubble from "./EventTileBubble";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
mxEvent: MatrixEvent
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.MKeyVerificationRequest")
@@ -18,6 +18,7 @@ import React from 'react';
import MImageBody from './MImageBody';
import * as sdk from '../../../index';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { BLURHASH_FIELD } from "../../../ContentMessages";
@replaceableComponent("views.messages.MStickerBody")
export default class MStickerBody extends MImageBody {
@@ -41,7 +42,8 @@ export default class MStickerBody extends MImageBody {
// Placeholder to show in place of the sticker image if
// img onLoad hasn't fired yet.
getPlaceholder() {
getPlaceholder(width, height) {
if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
}
+63 -14
View File
@@ -16,6 +16,8 @@ limitations under the License.
*/
import React from 'react';
import { decode } from "blurhash";
import MFileBody from './MFileBody';
import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
@@ -23,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import InlineSpinner from '../elements/InlineSpinner';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD } from "../../../ContentMessages";
interface IProps {
/* the MatrixEvent to show */
@@ -32,11 +35,13 @@ interface IProps {
}
interface IState {
decryptedUrl: string|null,
decryptedThumbnailUrl: string|null,
decryptedBlob: Blob|null,
error: any|null,
fetchingData: boolean,
decryptedUrl?: string;
decryptedThumbnailUrl?: string;
decryptedBlob?: Blob;
error?: any;
fetchingData: boolean;
posterLoading: boolean;
blurhashUrl: string;
}
@replaceableComponent("views.messages.MVideoBody")
@@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
decryptedThumbnailUrl: null,
decryptedBlob: null,
error: null,
posterLoading: false,
blurhashUrl: null,
};
}
thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy
@@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
private getThumbUrl(): string|null {
const content = this.props.mxEvent.getContent();
const media = mediaFromContent(content);
if (media.isEncrypted) {
if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
return this.state.decryptedThumbnailUrl;
} else if (this.state.posterLoading) {
return this.state.blurhashUrl;
} else if (media.hasThumbnail) {
return media.thumbnailHttp;
} else {
@@ -101,18 +111,57 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
}
}
private loadBlurhash() {
const info = this.props.mxEvent.getContent()?.info;
if (!info[BLURHASH_FIELD]) return;
const canvas = document.createElement("canvas");
let width = info.w;
let height = info.h;
const scale = this.thumbScale(info.w, info.h);
if (scale) {
width = Math.floor(info.w * scale);
height = Math.floor(info.h * scale);
}
canvas.width = width;
canvas.height = height;
const pixels = decode(info[BLURHASH_FIELD], width, height);
const ctx = canvas.getContext("2d");
const imgData = ctx.createImageData(width, height);
imgData.data.set(pixels);
ctx.putImageData(imgData, 0, 0);
this.setState({
blurhashUrl: canvas.toDataURL(),
posterLoading: true,
});
const content = this.props.mxEvent.getContent();
const media = mediaFromContent(content);
if (media.hasThumbnail) {
const image = new Image();
image.onload = () => {
this.setState({ posterLoading: false });
};
image.src = media.thumbnailHttp;
}
}
async componentDidMount() {
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
const content = this.props.mxEvent.getContent();
this.loadBlurhash();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);
if (content.info && content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
return URL.createObjectURL(blob);
});
if (content?.info?.thumbnail_file) {
thumbnailPromise = decryptFile(content.info.thumbnail_file)
.then(blob => URL.createObjectURL(blob));
}
try {
const thumbnailUrl = await thumbnailPromise;
if (autoplay) {
@@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
let poster = null;
let preload = "metadata";
if (content.info) {
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
const scale = this.thumbScale(content.info.w, content.info.h);
if (scale) {
width = Math.floor(content.info.w * scale);
height = Math.floor(content.info.h * scale);
@@ -67,7 +67,7 @@ interface IProps {
replacingEventId?: string;
/* callback for when our widget has loaded */
onHeightChanged(): void,
onHeightChanged(): void;
}
interface IState {
+13 -13
View File
@@ -32,32 +32,32 @@ import { throttle } from 'lodash';
interface IProps {
// js-sdk room object
room: Room,
userId: string,
showApps: boolean, // Render apps
room: Room;
userId: string;
showApps: boolean; // Render apps
// maxHeight attribute for the aux panel and the video
// therein
maxHeight: number,
maxHeight: number;
// a callback which is called when the content of the aux panel changes
// content in a way that is likely to make it change size.
onResize: () => void,
fullHeight: boolean,
onResize: () => void;
fullHeight: boolean;
resizeNotifier: ResizeNotifier,
resizeNotifier: ResizeNotifier;
}
interface Counter {
title: string,
value: number,
link: string,
severity: string,
stateKey: string,
title: string;
value: number;
link: string;
severity: string;
stateKey: string;
}
interface IState {
counters: Counter[],
counters: Counter[];
}
@replaceableComponent("views.rooms.AuxPanel")
+2 -2
View File
@@ -287,10 +287,10 @@ interface IProps {
permalinkCreator?: RoomPermalinkCreator;
// Symbol of the root node
as?: string
as?: string;
// whether or not to always show timestamps
alwaysShowTimestamps?: boolean
alwaysShowTimestamps?: boolean;
}
interface IState {
@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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.
@@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@@ -31,53 +30,64 @@ import RoomName from "../elements/RoomName";
import { PlaceCallType } from "../../../CallHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { throttle } from 'lodash';
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
import { E2EStatus } from '../../../utils/ShieldUtils';
import { IOOBData } from '../../../stores/ThreepidInviteStore';
import { SearchScope } from './SearchBar';
export interface ISearchInfo {
searchTerm: string;
searchScope: SearchScope;
searchCount: number;
}
interface IProps {
room: Room;
oobData?: IOOBData;
inRoom: boolean;
onSettingsClick: () => void;
onSearchClick: () => void;
onForgetClick: () => void;
onCallPlaced: (type: PlaceCallType) => void;
onAppsClick: () => void;
e2eStatus: E2EStatus;
appsShown: boolean;
searchInfo: ISearchInfo;
}
@replaceableComponent("views.rooms.RoomHeader")
export default class RoomHeader extends React.Component {
static propTypes = {
room: PropTypes.object,
oobData: PropTypes.object,
inRoom: PropTypes.bool,
onSettingsClick: PropTypes.func,
onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func,
e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func,
appsShown: PropTypes.bool,
onCallPlaced: PropTypes.func, // (PlaceCallType) => void;
};
export default class RoomHeader extends React.Component<IProps> {
static defaultProps = {
editing: false,
inRoom: false,
};
componentDidMount() {
public componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
cli.on("RoomState.events", this.onRoomStateEvents);
}
componentWillUnmount() {
public componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
cli.removeListener("RoomState.events", this.onRoomStateEvents);
}
}
_onRoomStateEvents = (event, state) => {
private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
}
// redisplay the room name, topic, etc.
this._rateLimitedUpdate();
this.rateLimitedUpdate();
};
_rateLimitedUpdate = throttle(() => {
private rateLimitedUpdate = throttle(() => {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
render() {
public render() {
let searchStatus = null;
// don't display the search count until the search completes and
@@ -22,7 +22,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/SpaceStore";
interface IProps {
onVisibilityChange?: () => void
onVisibilityChange?: () => void;
}
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
@@ -21,17 +21,17 @@ import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface ExistingSpellCheckLanguageIProps {
language: string,
onRemoved(language: string),
language: string;
onRemoved(language: string);
}
interface SpellCheckLanguagesIProps {
languages: Array<string>,
onLanguagesChange(languages: Array<string>),
languages: Array<string>;
onLanguagesChange(languages: Array<string>);
}
interface SpellCheckLanguagesIState {
newLanguage: string,
newLanguage: string;
}
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
@@ -23,7 +23,7 @@ import Field from "../elements/Field";
interface IProps {
avatarUrl?: string;
avatarDisabled?: boolean;
name?: string,
name?: string;
nameDisabled?: boolean;
topic?: string;
topicDisabled?: boolean;
+1 -1
View File
@@ -20,7 +20,7 @@ import { logger } from 'matrix-js-sdk/src/logger';
import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
interface IProps {
feed: CallFeed,
feed: CallFeed;
}
export default class AudioFeed extends React.Component<IProps> {
+11 -11
View File
@@ -35,10 +35,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// The call for us to display
call: MatrixCall,
call: MatrixCall;
// Another ongoing call to display information about
secondaryCall?: MatrixCall,
secondaryCall?: MatrixCall;
// a callback which is called when the content in the CallView changes
// in a way that is likely to cause a resize.
@@ -52,15 +52,15 @@ interface IProps {
}
interface IState {
isLocalOnHold: boolean,
isRemoteOnHold: boolean,
micMuted: boolean,
vidMuted: boolean,
callState: CallState,
controlsVisible: boolean,
showMoreMenu: boolean,
showDialpad: boolean,
feeds: CallFeed[],
isLocalOnHold: boolean;
isRemoteOnHold: boolean;
micMuted: boolean;
vidMuted: boolean;
callState: CallState;
controlsVisible: boolean;
showMoreMenu: boolean;
showDialpad: boolean;
feeds: CallFeed[];
}
function getFullScreenElement() {
@@ -25,16 +25,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// What room we should display the call for
roomId: string,
roomId: string;
// maxHeight style attribute for the video panel
maxVideoHeight?: number;
resizeNotifier: ResizeNotifier,
resizeNotifier: ResizeNotifier;
}
interface IState {
call: MatrixCall,
call: MatrixCall;
}
/*
+3 -3
View File
@@ -24,9 +24,9 @@ import MemberAvatar from "../avatars/MemberAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
call: MatrixCall,
call: MatrixCall;
feed: CallFeed,
feed: CallFeed;
// Whether this call view is for picture-in-picture mode
// otherwise, it's the larger call view when viewing the room the call is in.
@@ -36,7 +36,7 @@ interface IProps {
// a callback which is called when the video element is resized
// due to a change in video metadata
onResize?: (e: Event) => void,
onResize?: (e: Event) => void;
}
interface IState {
+6 -6
View File
@@ -69,11 +69,11 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
export interface ISecurityCustomisations {
examineLoginResponse?: typeof examineLoginResponse;
persistCredentials?: typeof persistCredentials;
createSecretStorageKey?: typeof createSecretStorageKey,
getSecretStorageKey?: typeof getSecretStorageKey,
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
setupEncryptionNeeded?: typeof setupEncryptionNeeded,
getDehydrationKey?: typeof getDehydrationKey,
createSecretStorageKey?: typeof createSecretStorageKey;
getSecretStorageKey?: typeof getSecretStorageKey;
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError;
setupEncryptionNeeded?: typeof setupEncryptionNeeded;
getDehydrationKey?: typeof getDehydrationKey;
/**
* When false, disables the post-login UI from showing. If there's
@@ -83,7 +83,7 @@ export interface ISecurityCustomisations {
* encryption is set up some other way which would circumvent the default
* UI, such as by presenting alternative UI.
*/
SHOW_ENCRYPTION_SETUP_UI?: boolean, // default true
SHOW_ENCRYPTION_SETUP_UI?: boolean; // default true
}
// A real customisation module will define and export one or more of the
@@ -20,7 +20,7 @@ import { ActionPayload } from "../payloads";
import { Action } from "../actions";
interface IBaseComposerInsertPayload extends ActionPayload {
action: Action.ComposerInsert,
action: Action.ComposerInsert;
}
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
+13 -13
View File
@@ -20,34 +20,34 @@ export type ConfettiOptions = {
/**
* max confetti count
*/
maxCount: number,
maxCount: number;
/**
* particle animation speed
*/
speed: number,
speed: number;
/**
* the confetti animation frame interval in milliseconds
*/
frameInterval: number,
frameInterval: number;
/**
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
*/
alpha: number,
alpha: number;
/**
* use gradient instead of solid particle color
*/
gradient: boolean,
gradient: boolean;
};
type ConfettiParticle = {
color: string,
color2: string,
x: number,
y: number,
diameter: number,
tilt: number,
tiltAngleIncrement: number,
tiltAngle: number,
color: string;
color2: string;
x: number;
y: number;
diameter: number;
tilt: number;
tiltAngleIncrement: number;
tiltAngle: number;
};
export const DefaultOptions: ConfettiOptions = {
+4 -4
View File
@@ -17,15 +17,15 @@ limitations under the License.
import { PerformanceEntryNames } from "./entry-names";
interface GetEntriesOptions {
name?: string,
type?: string,
name?: string;
type?: string;
}
type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void;
interface PerformanceDataListener {
entryNames?: string[],
callback: PerformanceCallbackFunction
entryNames?: string[];
callback: PerformanceCallbackFunction;
}
export default class PerformanceMonitor {
+10
View File
@@ -45,6 +45,16 @@ export interface IThreepidInvite {
inviterName: string;
}
// Any data about the room that would normally come from the homeserver
// but has been passed out-of-band, eg. the room name and avatar URL
// from an email invite (a workaround for the fact that we can't
// get this information from the HS using an email invite).
export interface IOOBData {
name?: string; // The room's name
avatarUrl?: string; // The mxc:// avatar URL for the room
inviterName?: string; // The display name of the person who invited us to the room
}
const STORAGE_PREFIX = "mx_threepid_invite_";
export default class ThreepidInviteStore extends EventEmitter {
+4 -4
View File
@@ -27,10 +27,10 @@ const TYPING_SERVER_TIMEOUT = 30000;
export default class TypingStore {
private typingStates: {
[roomId: string]: {
isTyping: boolean,
userTimer: Timer,
serverTimer: Timer,
},
isTyping: boolean;
userTimer: Timer;
serverTimer: Timer;
};
};
constructor() {
+2 -2
View File
@@ -26,8 +26,8 @@ import { WidgetType } from "../widgets/WidgetType";
class WidgetEchoStore extends EventEmitter {
private roomWidgetEcho: {
[roomId: string]: {
[widgetId: string]: IWidget,
},
[widgetId: string]: IWidget;
};
};
constructor() {
+1 -1
View File
@@ -152,7 +152,7 @@ export function objectClone<O extends {}>(obj: O): O {
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
const obj: {
// @ts-ignore - same as return type
[k: K]: V} = {};
[k: K]: V;} = {};
for (const e of entries) {
// @ts-ignore - same as return type
obj[e[0]] = e[1];