Merge remote-tracking branch 'origin/develop' into dbkr/tests_lint
commit
536e3aadd7
|
@ -55,6 +55,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
|
"blurhash": "^1.1.3",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"cheerio": "^1.0.0-rc.9",
|
"cheerio": "^1.0.0-rc.9",
|
||||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
.mx_MImageBody {
|
.mx_MImageBody {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 34px;
|
margin-right: 34px;
|
||||||
|
@ -25,7 +27,11 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: 4px;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail_container {
|
.mx_MImageBody_thumbnail_container {
|
||||||
|
|
|
@ -17,9 +17,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import dis from './dispatcher/dispatcher';
|
import { encode } from "blurhash";
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
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';
|
||||||
|
@ -47,6 +48,10 @@ const MAX_HEIGHT = 600;
|
||||||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||||
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
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 {}
|
export class UploadCanceledError extends Error {}
|
||||||
|
|
||||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||||
|
@ -77,6 +82,7 @@ interface IThumbnail {
|
||||||
};
|
};
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
|
[BLURHASH_FIELD]: string;
|
||||||
};
|
};
|
||||||
thumbnail: Blob;
|
thumbnail: Blob;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +130,16 @@ function createThumbnail(
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = targetWidth;
|
canvas.width = targetWidth;
|
||||||
canvas.height = targetHeight;
|
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) {
|
canvas.toBlob(function(thumbnail) {
|
||||||
resolve({
|
resolve({
|
||||||
info: {
|
info: {
|
||||||
|
@ -136,8 +151,9 @@ function createThumbnail(
|
||||||
},
|
},
|
||||||
w: inputWidth,
|
w: inputWidth,
|
||||||
h: inputHeight,
|
h: inputHeight,
|
||||||
|
[BLURHASH_FIELD]: blurhash,
|
||||||
},
|
},
|
||||||
thumbnail: thumbnail,
|
thumbnail,
|
||||||
});
|
});
|
||||||
}, mimeType);
|
}, 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.
|
* @param {File} videoFile The file to load in an video element.
|
||||||
* @return {Promise} A promise that resolves with the video image 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
video.preload = "metadata";
|
||||||
|
video.playsInline = true;
|
||||||
|
video.muted = true;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function(ev) {
|
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.
|
// Wait until we have enough data to thumbnail the first frame.
|
||||||
video.onloadeddata = function() {
|
video.onloadeddata = async function() {
|
||||||
resolve(video);
|
resolve(video);
|
||||||
|
video.pause();
|
||||||
};
|
};
|
||||||
video.onerror = function(e) {
|
video.onerror = function(e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
video.src = ev.target.result as string;
|
||||||
|
video.load();
|
||||||
|
video.play();
|
||||||
};
|
};
|
||||||
reader.onerror = function(e) {
|
reader.onerror = function(e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
@ -347,7 +369,7 @@ export function uploadFile(
|
||||||
});
|
});
|
||||||
(prom as IAbortablePromise<any>).abort = () => {
|
(prom as IAbortablePromise<any>).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
|
||||||
};
|
};
|
||||||
return prom;
|
return prom;
|
||||||
} else {
|
} else {
|
||||||
|
@ -357,11 +379,11 @@ export function uploadFile(
|
||||||
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.
|
||||||
return { "url": url };
|
return { url };
|
||||||
});
|
});
|
||||||
(promise1 as any).abort = () => {
|
(promise1 as any).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
matrixClient.cancelUpload(basePromise);
|
||||||
};
|
};
|
||||||
return promise1;
|
return promise1;
|
||||||
}
|
}
|
||||||
|
@ -373,7 +395,7 @@ export default class ContentMessages {
|
||||||
|
|
||||||
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
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);
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
throw 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
|
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');
|
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||||
await this.ensureMediaConfigFetched();
|
await this.ensureMediaConfigFetched(matrixClient);
|
||||||
modal.close();
|
modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,7 +492,7 @@ export default class ContentMessages {
|
||||||
return this.inprogress.filter(u => !u.canceled);
|
return this.inprogress.filter(u => !u.canceled);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload(promise: Promise<any>) {
|
cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
|
||||||
let upload: IUpload;
|
let upload: IUpload;
|
||||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||||
if (this.inprogress[i].promise === promise) {
|
if (this.inprogress[i].promise === promise) {
|
||||||
|
@ -480,7 +502,7 @@ export default class ContentMessages {
|
||||||
}
|
}
|
||||||
if (upload) {
|
if (upload) {
|
||||||
upload.canceled = true;
|
upload.canceled = true;
|
||||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
matrixClient.cancelUpload(upload.promise);
|
||||||
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -621,11 +643,11 @@ export default class ContentMessages {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureMediaConfigFetched() {
|
private ensureMediaConfigFetched(matrixClient: MatrixClient) {
|
||||||
if (this.mediaConfig !== null) return;
|
if (this.mediaConfig !== null) return;
|
||||||
|
|
||||||
console.log("[Media Config] Fetching");
|
console.log("[Media Config] Fetching");
|
||||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
return matrixClient.getMediaConfig().then((config) => {
|
||||||
console.log("[Media Config] Fetched config:", config);
|
console.log("[Media Config] Fetched config:", config);
|
||||||
return config;
|
return config;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|
|
@ -48,7 +48,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
|
||||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||||
|
@ -81,7 +81,7 @@ interface IProps {
|
||||||
page_type: string;
|
page_type: string;
|
||||||
autoJoin: boolean;
|
autoJoin: boolean;
|
||||||
threepidInvite?: IThreepidInvite;
|
threepidInvite?: IThreepidInvite;
|
||||||
roomOobData?: object;
|
roomOobData?: IOOBData;
|
||||||
currentRoomId: string;
|
currentRoomId: string;
|
||||||
collapseLhs: boolean;
|
collapseLhs: boolean;
|
||||||
config: {
|
config: {
|
||||||
|
|
|
@ -370,7 +370,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onFilterChange = (alias: string) => {
|
private onFilterChange = (alias: string) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: alias || null,
|
filterString: alias || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// don't send the request for a little bit,
|
// don't send the request for a little bit,
|
||||||
|
@ -389,7 +389,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
private onFilterClear = () => {
|
private onFilterClear = () => {
|
||||||
// update immediately
|
// update immediately
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: null,
|
filterString: "",
|
||||||
}, this.refreshRoomList);
|
}, this.refreshRoomList);
|
||||||
|
|
||||||
if (this.filterTimeout) {
|
if (this.filterTimeout) {
|
||||||
|
|
|
@ -63,7 +63,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||||
import { containsEmoji } from '../../effects/utils';
|
import { containsEmoji } from '../../effects/utils';
|
||||||
import { CHAT_EFFECTS } from '../../effects';
|
import { CHAT_EFFECTS } from '../../effects';
|
||||||
|
@ -95,21 +95,7 @@ if (DEBUG) {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
threepidInvite: IThreepidInvite,
|
threepidInvite: IThreepidInvite,
|
||||||
|
oobData?: IOOBData;
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
justCreatedOpts?: IOpts;
|
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()) {
|
if (this.context.isGuest()) {
|
||||||
dis.dispatch({ action: 'require_registration' });
|
dis.dispatch({ action: 'require_registration' });
|
||||||
return;
|
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 = () => {
|
private onForgetClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'forget_room',
|
action: 'forget_room',
|
||||||
|
@ -2106,7 +2085,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
||||||
appsShown={this.state.showApps}
|
appsShown={this.state.showApps}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import { IUpload } from "../../models/IUpload";
|
import { IUpload } from "../../models/IUpload";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -38,6 +39,8 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("structures.UploadBar")
|
@replaceableComponent("structures.UploadBar")
|
||||||
export default class UploadBar extends React.Component<IProps, IState> {
|
export default class UploadBar extends React.Component<IProps, IState> {
|
||||||
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private mounted: boolean;
|
private mounted: boolean;
|
||||||
|
|
||||||
|
@ -82,7 +85,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onCancelClick = (ev) => {
|
private onCancelClick = (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
|
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler";
|
||||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
avatarSize: number;
|
avatarSize: number;
|
||||||
displayBadge?: boolean;
|
displayBadge?: boolean;
|
||||||
forceCount?: boolean;
|
forceCount?: boolean;
|
||||||
oobData?: object;
|
oobData?: IOOBData;
|
||||||
viewAvatarOnClick?: boolean;
|
viewAvatarOnClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,14 +24,14 @@ import Modal from '../../../Modal';
|
||||||
import * as Avatar from '../../../Avatar';
|
import * as Avatar from '../../../Avatar';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
room?: Room;
|
room?: Room;
|
||||||
// TODO: type when js-sdk has types
|
oobData?: IOOBData;
|
||||||
oobData?: any;
|
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
resizeMethod?: ResizeMethod;
|
resizeMethod?: ResizeMethod;
|
||||||
|
|
|
@ -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,6 +29,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { mediaFromContent } from "../../../customisations/Media";
|
||||||
|
import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
|
||||||
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MImageBody")
|
@replaceableComponent("views.messages.MImageBody")
|
||||||
export default class MImageBody extends React.Component {
|
export default class MImageBody extends React.Component {
|
||||||
|
@ -333,7 +335,8 @@ export default class MImageBody extends React.Component {
|
||||||
infoWidth = content.info.w;
|
infoWidth = content.info.w;
|
||||||
infoHeight = content.info.h;
|
infoHeight = content.info.h;
|
||||||
} else {
|
} 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`.
|
// 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 placeholder = null;
|
||||||
let gifLabel = null;
|
let gifLabel = null;
|
||||||
|
|
||||||
// e2e image hasn't been decrypted yet
|
if (!this.state.imgLoaded) {
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||||
placeholder = <InlineSpinner w={32} h={32} />;
|
|
||||||
} else if (!this.state.imgLoaded) {
|
|
||||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
|
||||||
placeholder = this.getPlaceholder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let showPlaceholder = Boolean(placeholder);
|
let showPlaceholder = Boolean(placeholder);
|
||||||
|
@ -395,7 +394,7 @@ export default class MImageBody extends React.Component {
|
||||||
|
|
||||||
if (!this.state.showImage) {
|
if (!this.state.showImage) {
|
||||||
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
|
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) {
|
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
|
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||||
maxWidth: infoWidth + "px",
|
maxWidth: infoWidth + "px",
|
||||||
}}>
|
}}>
|
||||||
<div className="mx_MImageBody_thumbnail_spinner">
|
{ placeholder }
|
||||||
{ placeholder }
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,9 +434,12 @@ export default class MImageBody extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
getPlaceholder() {
|
getPlaceholder(width, height) {
|
||||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||||
return null;
|
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
|
// Overidden by MStickerBody
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import MImageBody from './MImageBody';
|
import MImageBody from './MImageBody';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MStickerBody")
|
@replaceableComponent("views.messages.MStickerBody")
|
||||||
export default class MStickerBody extends MImageBody {
|
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
|
// Placeholder to show in place of the sticker image if
|
||||||
// img onLoad hasn't fired yet.
|
// 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" />;
|
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { decode } from "blurhash";
|
||||||
|
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
import { decryptFile } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -23,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { mediaFromContent } from "../../../customisations/Media";
|
||||||
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
|
@ -32,11 +35,13 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
decryptedUrl: string|null,
|
decryptedUrl?: string;
|
||||||
decryptedThumbnailUrl: string|null,
|
decryptedThumbnailUrl?: string;
|
||||||
decryptedBlob: Blob|null,
|
decryptedBlob?: Blob;
|
||||||
error: any|null,
|
error?: any;
|
||||||
fetchingData: boolean,
|
fetchingData: boolean;
|
||||||
|
posterLoading: boolean;
|
||||||
|
blurhashUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MVideoBody")
|
@replaceableComponent("views.messages.MVideoBody")
|
||||||
|
@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
decryptedThumbnailUrl: null,
|
decryptedThumbnailUrl: null,
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
error: 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) {
|
if (!fullWidth || !fullHeight) {
|
||||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||||
// log this because it's spammy
|
// log this because it's spammy
|
||||||
|
@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
private getThumbUrl(): string|null {
|
private getThumbUrl(): string|null {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
const media = mediaFromContent(content);
|
const media = mediaFromContent(content);
|
||||||
if (media.isEncrypted) {
|
|
||||||
|
if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
|
} else if (this.state.posterLoading) {
|
||||||
|
return this.state.blurhashUrl;
|
||||||
} else if (media.hasThumbnail) {
|
} else if (media.hasThumbnail) {
|
||||||
return media.thumbnailHttp;
|
return media.thumbnailHttp;
|
||||||
} else {
|
} 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() {
|
async componentDidMount() {
|
||||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
|
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
this.loadBlurhash();
|
||||||
|
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
let thumbnailPromise = Promise.resolve(null);
|
let thumbnailPromise = Promise.resolve(null);
|
||||||
if (content.info && content.info.thumbnail_file) {
|
if (content?.info?.thumbnail_file) {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(content.info.thumbnail_file)
|
||||||
content.info.thumbnail_file,
|
.then(blob => URL.createObjectURL(blob));
|
||||||
).then(function(blob) {
|
|
||||||
return URL.createObjectURL(blob);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thumbnailUrl = await thumbnailPromise;
|
const thumbnailUrl = await thumbnailPromise;
|
||||||
if (autoplay) {
|
if (autoplay) {
|
||||||
|
@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
let poster = null;
|
let poster = null;
|
||||||
let preload = "metadata";
|
let preload = "metadata";
|
||||||
if (content.info) {
|
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) {
|
if (scale) {
|
||||||
width = Math.floor(content.info.w * scale);
|
width = Math.floor(content.info.w * scale);
|
||||||
height = Math.floor(content.info.h * scale);
|
height = Math.floor(content.info.h * scale);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
@ -31,53 +30,64 @@ import RoomName from "../elements/RoomName";
|
||||||
import { PlaceCallType } from "../../../CallHandler";
|
import { PlaceCallType } from "../../../CallHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { throttle } from 'lodash';
|
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")
|
@replaceableComponent("views.rooms.RoomHeader")
|
||||||
export default class RoomHeader extends React.Component {
|
export default class RoomHeader extends React.Component<IProps> {
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
editing: false,
|
editing: false,
|
||||||
inRoom: false,
|
inRoom: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
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) {
|
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// redisplay the room name, topic, etc.
|
// redisplay the room name, topic, etc.
|
||||||
this._rateLimitedUpdate();
|
this.rateLimitedUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
_rateLimitedUpdate = throttle(() => {
|
private rateLimitedUpdate = throttle(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500, { leading: true, trailing: true });
|
}, 500, { leading: true, trailing: true });
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
let searchStatus = null;
|
let searchStatus = null;
|
||||||
|
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
|
@ -45,6 +45,16 @@ export interface IThreepidInvite {
|
||||||
inviterName: string;
|
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_";
|
const STORAGE_PREFIX = "mx_threepid_invite_";
|
||||||
|
|
||||||
export default class ThreepidInviteStore extends EventEmitter {
|
export default class ThreepidInviteStore extends EventEmitter {
|
||||||
|
|
|
@ -2190,6 +2190,11 @@ bluebird@^3.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
|
blurhash@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||||
|
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||||
|
|
||||||
boolbase@^1.0.0:
|
boolbase@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||||
|
|
Loading…
Reference in New Issue