Merge remote-tracking branch 'origin/release-v3.43.0'
# Conflicts: # CHANGELOG.md # package.json # src/components/views/messages/MessageActionBar.tsx
This commit is contained in:
commit
6b0156d876
680 changed files with 12433 additions and 5108 deletions
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
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>;
|
||||
}
|
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
|
@ -29,7 +29,6 @@ import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
|
|||
import { IntegrationManagers } from "../integrations/IntegrationManagers";
|
||||
import { ModalManager } from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { ActiveRoomObserver } from "../ActiveRoomObserver";
|
||||
import { Notifier } from "../Notifier";
|
||||
import type { Renderer } from "react-dom";
|
||||
import RightPanelStore from "../stores/right-panel/RightPanelStore";
|
||||
|
@ -50,7 +49,6 @@ import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
|||
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||
import { Skinner } from "../Skinner";
|
||||
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
|
||||
import { IConfigOptions } from "../IConfigOptions";
|
||||
|
||||
|
@ -83,7 +81,6 @@ declare global {
|
|||
mxDeviceListener: DeviceListener;
|
||||
mxRoomListStore: RoomListStoreClass;
|
||||
mxRoomListLayoutStore: RoomListLayoutStore;
|
||||
mxActiveRoomObserver: ActiveRoomObserver;
|
||||
mxPlatformPeg: PlatformPeg;
|
||||
mxIntegrationManagers: typeof IntegrationManagers;
|
||||
singletonModalManager: ModalManager;
|
||||
|
@ -107,7 +104,6 @@ declare global {
|
|||
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||
mxRoomScrollStateStore?: RoomScrollStateStore;
|
||||
mxActiveWidgetStore?: ActiveWidgetStore;
|
||||
mxSkinner?: Skinner;
|
||||
mxOnRecaptchaLoaded?: () => void;
|
||||
electron?: Electron;
|
||||
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 - 2022 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 { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
|
||||
type Listener = (isActive: boolean) => void;
|
||||
|
||||
/**
|
||||
* Consumes changes from the RoomViewStore and notifies specific things
|
||||
* about when the active room changes. Unlike listening for RoomViewStore
|
||||
* changes, you can subscribe to only changes relevant to a particular
|
||||
* room.
|
||||
*
|
||||
* TODO: If we introduce an observer for something else, factor out
|
||||
* the adding / removing of listeners & emitting into a common class.
|
||||
*/
|
||||
export class ActiveRoomObserver {
|
||||
private listeners: {[key: string]: Listener[]} = {};
|
||||
private _activeRoomId = RoomViewStore.getRoomId();
|
||||
|
||||
constructor() {
|
||||
// TODO: We could self-destruct when the last listener goes away, or at least stop listening.
|
||||
RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
}
|
||||
|
||||
public get activeRoomId(): string {
|
||||
return this._activeRoomId;
|
||||
}
|
||||
|
||||
public addListener(roomId, listener) {
|
||||
if (!this.listeners[roomId]) this.listeners[roomId] = [];
|
||||
this.listeners[roomId].push(listener);
|
||||
}
|
||||
|
||||
public removeListener(roomId, listener) {
|
||||
if (this.listeners[roomId]) {
|
||||
const i = this.listeners[roomId].indexOf(listener);
|
||||
if (i > -1) {
|
||||
this.listeners[roomId].splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unregistering unrecognised listener (roomId=" + roomId + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private emit(roomId, isActive: boolean) {
|
||||
if (!this.listeners[roomId]) return;
|
||||
|
||||
for (const l of this.listeners[roomId]) {
|
||||
l.call(null, isActive);
|
||||
}
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
// emit for the old room ID
|
||||
if (this._activeRoomId) this.emit(this._activeRoomId, false);
|
||||
|
||||
// update our cache
|
||||
this._activeRoomId = RoomViewStore.getRoomId();
|
||||
|
||||
// and emit for the new one
|
||||
if (this._activeRoomId) this.emit(this._activeRoomId, true);
|
||||
};
|
||||
}
|
||||
|
||||
if (window.mxActiveRoomObserver === undefined) {
|
||||
window.mxActiveRoomObserver = new ActiveRoomObserver();
|
||||
}
|
||||
export default window.mxActiveRoomObserver;
|
|
@ -23,7 +23,7 @@ import { getCurrentLanguage, _t, _td, IVariables } from './languageHandler';
|
|||
import PlatformPeg from './PlatformPeg';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import { SnakedObject } from "./utils/SnakedObject";
|
||||
import { IConfigOptions } from "./IConfigOptions";
|
||||
|
||||
|
@ -406,14 +406,12 @@ export class Analytics {
|
|||
{ expl: _td('Your device resolution'), value: resolution },
|
||||
];
|
||||
|
||||
// FIXME: Using an import will result in test failures
|
||||
const piwikConfig = SdkConfig.get("piwik");
|
||||
let piwik: Optional<SnakedObject<Extract<IConfigOptions["piwik"], object>>>;
|
||||
if (typeof piwikConfig === 'object') {
|
||||
piwik = new SnakedObject(piwikConfig);
|
||||
}
|
||||
const cookiePolicyUrl = piwik?.get("policy_url");
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
const cookiePolicyLink = _t(
|
||||
"Our complete cookie policy can be found <CookiePolicyLink>here</CookiePolicyLink>.",
|
||||
{},
|
||||
|
|
|
@ -17,9 +17,11 @@ limitations under the License.
|
|||
import React, { ComponentType } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
|
||||
import BaseDialog from "./components/views/dialogs/BaseDialog";
|
||||
import DialogButtons from "./components/views/elements/DialogButtons";
|
||||
import Spinner from "./components/views/elements/Spinner";
|
||||
|
||||
type AsyncImport<T> = { default: T };
|
||||
|
||||
|
@ -78,9 +80,6 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
|
|||
const Component = this.state.component;
|
||||
return <Component {...this.props} />;
|
||||
} else if (this.state.error) {
|
||||
// FIXME: Using an import will result in test failures
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
|
||||
{ _t("Unable to load! Check your network connectivity and try again.") }
|
||||
<DialogButtons primaryButton={_t("Dismiss")}
|
||||
|
@ -90,7 +89,6 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
|
|||
</BaseDialog>;
|
||||
} else {
|
||||
// show a spinner until the component is loaded.
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ function isValidHexColor(color: string): boolean {
|
|||
return typeof color === "string" &&
|
||||
(color.length === 7 || color.length === 9) &&
|
||||
color.charAt(0) === "#" &&
|
||||
!color.substr(1).split("").some(c => isNaN(parseInt(c, 16)));
|
||||
!color.slice(1).split("").some(c => isNaN(parseInt(c, 16)));
|
||||
}
|
||||
|
||||
function urlForColor(color: string): string {
|
||||
|
|
|
@ -145,6 +145,13 @@ export default abstract class BasePlatform {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if platform allows overriding native context menus
|
||||
*/
|
||||
public allowOverridingNativeContextMenus(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the platform supports displaying
|
||||
* notifications, otherwise false.
|
||||
|
|
|
@ -44,7 +44,6 @@ import { WidgetType } from "./widgets/WidgetType";
|
|||
import { SettingLevel } from "./settings/SettingLevel";
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import InviteDialog, { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialog";
|
||||
import WidgetStore from "./stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||
|
@ -54,12 +53,15 @@ import { Action } from './dispatcher/actions';
|
|||
import VoipUserMapper from './VoipUserMapper';
|
||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||
import { ensureDMExists } from './createRoom';
|
||||
import { Container, WidgetLayoutStore } from './stores/widgets/WidgetLayoutStore';
|
||||
import IncomingCallToast, { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
||||
import ToastStore from './stores/ToastStore';
|
||||
import Resend from './Resend';
|
||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||
import { findDMForUser } from "./utils/direct-messages";
|
||||
import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes";
|
||||
import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload";
|
||||
|
||||
export const PROTOCOL_PSTN = 'm.protocol.pstn';
|
||||
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
|
||||
|
@ -67,9 +69,6 @@ export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native';
|
|||
export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
|
||||
|
||||
const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
||||
// Event type for room account data and room creation content used to mark rooms as virtual rooms
|
||||
// (and store the ID of their native room)
|
||||
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
|
@ -1094,14 +1093,17 @@ export default class CallHandler extends EventEmitter {
|
|||
*/
|
||||
public showTransferDialog(call: MatrixCall): void {
|
||||
call.setRemoteOnHold(true);
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call },
|
||||
/*className=*/"mx_InviteDialog_transferWrapper", /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
finished.then((results: boolean[]) => {
|
||||
if (results.length === 0 || results[0] === false) {
|
||||
call.setRemoteOnHold(false);
|
||||
}
|
||||
dis.dispatch<OpenInviteDialogPayload>({
|
||||
action: Action.OpenInviteDialog,
|
||||
kind: KIND_CALL_TRANSFER,
|
||||
call,
|
||||
analyticsName: "Transfer Call",
|
||||
className: "mx_InviteDialog_transferWrapper",
|
||||
onFinishedCallback: (results) => {
|
||||
if (results.length === 0 || results[0] === false) {
|
||||
call.setRemoteOnHold(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { IUploadOpts } from "matrix-js-sdk/src/@types/requests";
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import encrypt from "browser-encrypt-attachment";
|
||||
import encrypt from "matrix-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";
|
||||
|
@ -28,7 +28,6 @@ import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
|||
|
||||
import { IEncryptedFile, IMediaEventInfo } from "./customisations/models/IMediaEventContent";
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import Spinner from "./components/views/elements/Spinner";
|
||||
|
@ -41,27 +40,23 @@ import {
|
|||
UploadStartedPayload,
|
||||
} from "./dispatcher/payloads/UploadPayload";
|
||||
import { IUpload } from "./models/IUpload";
|
||||
import { BlurhashEncoder } from "./BlurhashEncoder";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
|
||||
import { TimelineRenderingType } from "./contexts/RoomContext";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { RoomViewStore } from "./stores/RoomViewStore";
|
||||
import { addReplyToMessageContent } from "./utils/Reply";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog";
|
||||
import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog";
|
||||
import { createThumbnail } from "./utils/image-media";
|
||||
import { attachRelation } from "./components/views/rooms/SendMessageComposer";
|
||||
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
|
||||
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
||||
// 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
|
||||
|
||||
export class UploadCanceledError extends Error {}
|
||||
|
||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||
|
||||
interface IMediaConfig {
|
||||
"m.upload.size"?: number;
|
||||
}
|
||||
|
@ -77,103 +72,6 @@ interface IContent {
|
|||
url?: string;
|
||||
}
|
||||
|
||||
interface IThumbnail {
|
||||
info: {
|
||||
// eslint-disable-next-line camelcase
|
||||
thumbnail_info: {
|
||||
w: number;
|
||||
h: number;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
};
|
||||
w: number;
|
||||
h: number;
|
||||
[BLURHASH_FIELD]: string;
|
||||
};
|
||||
thumbnail: Blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thumbnail for a image DOM element.
|
||||
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
|
||||
* The thumbnail will have the same aspect ratio as the original.
|
||||
* Draws the element into a canvas using CanvasRenderingContext2D.drawImage
|
||||
* Then calls Canvas.toBlob to get a blob object for the image data.
|
||||
*
|
||||
* Since it needs to calculate the dimensions of the source image and the
|
||||
* thumbnailed image it returns an info object filled out with information
|
||||
* about the original image and the thumbnail.
|
||||
*
|
||||
* @param {HTMLElement} element The element to thumbnail.
|
||||
* @param {number} inputWidth The width of the image in the input element.
|
||||
* @param {number} inputHeight the width of the image in the input element.
|
||||
* @param {string} mimeType The mimeType to save the blob as.
|
||||
* @param {boolean} calculateBlurhash Whether to calculate a blurhash of the given image too.
|
||||
* @return {Promise} A promise that resolves with an object with an info key
|
||||
* and a thumbnail key.
|
||||
*/
|
||||
export async function createThumbnail(
|
||||
element: ThumbnailableElement,
|
||||
inputWidth: number,
|
||||
inputHeight: number,
|
||||
mimeType: string,
|
||||
calculateBlurhash = true,
|
||||
): Promise<IThumbnail> {
|
||||
let targetWidth = inputWidth;
|
||||
let targetHeight = inputHeight;
|
||||
if (targetHeight > MAX_HEIGHT) {
|
||||
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
||||
targetHeight = MAX_HEIGHT;
|
||||
}
|
||||
if (targetWidth > MAX_WIDTH) {
|
||||
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
|
||||
targetWidth = MAX_WIDTH;
|
||||
}
|
||||
|
||||
let canvas: HTMLCanvasElement | OffscreenCanvas;
|
||||
let context: CanvasRenderingContext2D;
|
||||
try {
|
||||
canvas = new window.OffscreenCanvas(targetWidth, targetHeight);
|
||||
context = canvas.getContext("2d");
|
||||
} catch (e) {
|
||||
// Fallback support for other browsers (Safari and Firefox for now)
|
||||
canvas = document.createElement("canvas");
|
||||
(canvas as HTMLCanvasElement).width = targetWidth;
|
||||
(canvas as HTMLCanvasElement).height = targetHeight;
|
||||
context = canvas.getContext("2d");
|
||||
}
|
||||
|
||||
context.drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||
|
||||
let thumbnailPromise: Promise<Blob>;
|
||||
|
||||
if (window.OffscreenCanvas) {
|
||||
thumbnailPromise = (canvas as OffscreenCanvas).convertToBlob({ type: mimeType });
|
||||
} else {
|
||||
thumbnailPromise = new Promise<Blob>(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType));
|
||||
}
|
||||
|
||||
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
||||
// thumbnailPromise and blurhash promise are being awaited concurrently
|
||||
const blurhash = calculateBlurhash ? await BlurhashEncoder.instance.getBlurhash(imageData) : undefined;
|
||||
const thumbnail = await thumbnailPromise;
|
||||
|
||||
return {
|
||||
info: {
|
||||
thumbnail_info: {
|
||||
w: targetWidth,
|
||||
h: targetHeight,
|
||||
mimetype: thumbnail.type,
|
||||
size: thumbnail.size,
|
||||
},
|
||||
w: inputWidth,
|
||||
h: inputHeight,
|
||||
[BLURHASH_FIELD]: blurhash,
|
||||
},
|
||||
thumbnail,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a file into a newly created image element.
|
||||
*
|
||||
|
@ -472,7 +370,7 @@ export default class ContentMessages {
|
|||
return;
|
||||
}
|
||||
|
||||
const replyToEvent = RoomViewStore.getQuotingEvent();
|
||||
const replyToEvent = RoomViewStore.instance.getQuotingEvent();
|
||||
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(matrixClient);
|
||||
|
@ -491,8 +389,6 @@ export default class ContentMessages {
|
|||
}
|
||||
|
||||
if (tooBigFiles.length > 0) {
|
||||
// FIXME: Using an import will result in Element crashing
|
||||
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
|
||||
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, {
|
||||
badFiles: tooBigFiles,
|
||||
totalFiles: files.length,
|
||||
|
@ -509,8 +405,6 @@ export default class ContentMessages {
|
|||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
if (!uploadAll) {
|
||||
// FIXME: Using an import will result in Element crashing
|
||||
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
|
||||
const { finished } = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation',
|
||||
'', UploadConfirmDialog, {
|
||||
file,
|
||||
|
@ -689,8 +583,6 @@ export default class ContentMessages {
|
|||
{ fileName: upload.fileName },
|
||||
);
|
||||
}
|
||||
// FIXME: Using an import will result in Element crashing
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||
title: _t('Upload Failed'),
|
||||
description: desc,
|
||||
|
|
|
@ -36,9 +36,9 @@ import {
|
|||
} from "./toasts/UnverifiedSessionToast";
|
||||
import { accessSecretStorage, isSecretStorageBeingAccessed } from "./SecurityManager";
|
||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||
import { isLoggedIn } from './components/structures/MatrixChat';
|
||||
import { ActionPayload } from "./dispatcher/payloads";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import { isLoggedIn } from "./utils/login";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
|
|
|
@ -60,10 +60,19 @@ import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestore
|
|||
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog";
|
||||
import { setSentryUser } from "./sentry";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import { DialogOpener } from "./utils/DialogOpener";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
||||
dis.register((payload) => {
|
||||
if (payload.action === Action.TriggerLogout) {
|
||||
// noinspection JSIgnoredPromiseFromCall - we don't care if it fails
|
||||
onLoggedOut();
|
||||
}
|
||||
});
|
||||
|
||||
interface ILoadSessionOpts {
|
||||
enableGuest?: boolean;
|
||||
guestHsUrl?: string;
|
||||
|
@ -791,6 +800,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
|||
TypingStore.sharedInstance().reset();
|
||||
ToastStore.sharedInstance().reset();
|
||||
|
||||
DialogOpener.instance.prepare();
|
||||
Notifier.start();
|
||||
UserActivity.sharedInstance().start();
|
||||
DMRoomMap.makeShared().start();
|
||||
|
@ -865,8 +875,9 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
|||
Analytics.disable();
|
||||
|
||||
if (window.localStorage) {
|
||||
// try to save any 3pid invites from being obliterated
|
||||
// try to save any 3pid invites from being obliterated and registration time
|
||||
const pendingInvites = ThreepidInviteStore.instance.getWireInvites();
|
||||
const registrationTime = window.localStorage.getItem("mx_registration_time");
|
||||
|
||||
window.localStorage.clear();
|
||||
|
||||
|
@ -876,13 +887,17 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
|||
logger.error("idbDelete failed for account:mx_access_token", e);
|
||||
}
|
||||
|
||||
// now restore those invites
|
||||
// now restore those invites and registration time
|
||||
if (!opts?.deleteEverything) {
|
||||
pendingInvites.forEach(i => {
|
||||
const roomId = i.roomId;
|
||||
delete i.roomId; // delete to avoid confusing the store
|
||||
ThreepidInviteStore.instance.storeInvite(roomId, i);
|
||||
});
|
||||
|
||||
if (registrationTime) {
|
||||
window.localStorage.setItem("mx_registration_time", registrationTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ import { verificationMethods } from 'matrix-js-sdk/src/crypto';
|
|||
import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from './index';
|
||||
import createMatrixClient from './utils/createMatrixClient';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import MatrixActionCreators from './actions/MatrixActionCreators';
|
||||
|
@ -37,6 +36,7 @@ import * as StorageManager from './utils/StorageManager';
|
|||
import IdentityAuthClient from './IdentityAuthClient';
|
||||
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
|
||||
|
||||
export interface IMatrixClientCreds {
|
||||
homeserverUrl: string;
|
||||
|
@ -117,7 +117,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
|||
};
|
||||
|
||||
private matrixClient: MatrixClient = null;
|
||||
private justRegisteredUserId: string;
|
||||
private justRegisteredUserId: string | null = null;
|
||||
|
||||
// the credentials used to init the current client object.
|
||||
// used if we tear it down & recreate it with a different store
|
||||
|
@ -136,7 +136,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
|||
MatrixActionCreators.stop();
|
||||
}
|
||||
|
||||
public setJustRegisteredUserId(uid: string): void {
|
||||
public setJustRegisteredUserId(uid: string | null): void {
|
||||
this.justRegisteredUserId = uid;
|
||||
if (uid) {
|
||||
window.localStorage.setItem("mx_registration_time", String(new Date().getTime()));
|
||||
|
@ -151,9 +151,14 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
|||
}
|
||||
|
||||
public userRegisteredWithinLastHours(hours: number): boolean {
|
||||
if (hours <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const date = new Date(window.localStorage.getItem("mx_registration_time"));
|
||||
return ((new Date().getTime() - date.getTime()) / 36e5) <= hours;
|
||||
const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time"));
|
||||
const diff = Date.now() - registrationTime;
|
||||
return (diff / 36e5) <= hours;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
@ -200,9 +205,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
|||
} catch (e) {
|
||||
if (e && e.name === 'InvalidCryptoStoreError') {
|
||||
// The js-sdk found a crypto DB too new for it to use
|
||||
// FIXME: Using an import will result in test failures
|
||||
const CryptoStoreTooNewDialog =
|
||||
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
|
||||
Modal.createDialog(CryptoStoreTooNewDialog);
|
||||
}
|
||||
// this can happen for a number of reasons, the most likely being
|
||||
|
|
|
@ -37,7 +37,7 @@ import SettingsStore from "./settings/SettingsStore";
|
|||
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
|
||||
import { SettingLevel } from "./settings/SettingLevel";
|
||||
import { isPushNotifyDisabled } from "./settings/controllers/NotificationControllers";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { RoomViewStore } from "./stores/RoomViewStore";
|
||||
import UserActivity from "./UserActivity";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
|
@ -401,7 +401,7 @@ export const Notifier = {
|
|||
|
||||
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||
if (actions?.notify) {
|
||||
if (RoomViewStore.getRoomId() === room.roomId &&
|
||||
if (RoomViewStore.instance.getRoomId() === room.roomId &&
|
||||
UserActivity.sharedInstance().userActiveRecently() &&
|
||||
!Modal.hasDialogs()
|
||||
) {
|
||||
|
|
|
@ -24,10 +24,12 @@ import { MatrixClientPeg } from './MatrixClientPeg';
|
|||
import MultiInviter, { CompletionStates } from './utils/MultiInviter';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog";
|
||||
import InviteDialog from "./components/views/dialogs/InviteDialog";
|
||||
import BaseAvatar from "./components/views/avatars/BaseAvatar";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import { KIND_DM, KIND_INVITE } from "./components/views/dialogs/InviteDialogTypes";
|
||||
import { Member } from "./utils/direct-messages";
|
||||
|
||||
export interface IInviteResult {
|
||||
states: CompletionStates;
|
||||
|
|
48
src/Rooms.ts
48
src/Rooms.ts
|
@ -19,9 +19,6 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import AliasCustomisations from './customisations/Alias';
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import SpaceStore from "./stores/spaces/SpaceStore";
|
||||
import { _t } from "./languageHandler";
|
||||
|
||||
/**
|
||||
* Given a room object, return the alias we should use for it,
|
||||
|
@ -157,48 +154,3 @@ function guessDMRoomTargetId(room: Room, myUserId: string): string {
|
|||
if (oldestUser === undefined) return myUserId;
|
||||
return oldestUser.userId;
|
||||
}
|
||||
|
||||
export function spaceContextDetailsText(space: Room): string {
|
||||
if (!space.isSpaceRoom()) return undefined;
|
||||
|
||||
const [parent, secondParent, ...otherParents] = SpaceStore.instance.getKnownParents(space.roomId);
|
||||
if (secondParent && !otherParents?.length) {
|
||||
// exactly 2 edge case for improved i18n
|
||||
return _t("%(space1Name)s and %(space2Name)s", {
|
||||
space1Name: space.client.getRoom(parent)?.name,
|
||||
space2Name: space.client.getRoom(secondParent)?.name,
|
||||
});
|
||||
} else if (parent) {
|
||||
return _t("%(spaceName)s and %(count)s others", {
|
||||
spaceName: space.client.getRoom(parent)?.name,
|
||||
count: otherParents.length,
|
||||
});
|
||||
}
|
||||
|
||||
return space.getCanonicalAlias();
|
||||
}
|
||||
|
||||
export function roomContextDetailsText(room: Room): string {
|
||||
if (room.isSpaceRoom()) return undefined;
|
||||
|
||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (dmPartner) {
|
||||
return dmPartner;
|
||||
}
|
||||
|
||||
const [parent, secondParent, ...otherParents] = SpaceStore.instance.getKnownParents(room.roomId);
|
||||
if (secondParent && !otherParents?.length) {
|
||||
// exactly 2 edge case for improved i18n
|
||||
return _t("%(space1Name)s and %(space2Name)s", {
|
||||
space1Name: room.client.getRoom(parent)?.name,
|
||||
space2Name: room.client.getRoom(secondParent)?.name,
|
||||
});
|
||||
} else if (parent) {
|
||||
return _t("%(spaceName)s and %(count)s others", {
|
||||
spaceName: room.client.getRoom(parent)?.name,
|
||||
count: otherParents.length,
|
||||
});
|
||||
}
|
||||
|
||||
return room.getCanonicalAlias();
|
||||
}
|
||||
|
|
|
@ -241,7 +241,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
import { RoomViewStore } from './stores/RoomViewStore';
|
||||
import { _t } from './languageHandler';
|
||||
import { IntegrationManagers } from "./integrations/IntegrationManagers";
|
||||
import { WidgetType } from "./widgets/WidgetType";
|
||||
|
@ -638,7 +638,7 @@ const onMessage = function(event: MessageEvent<any>): void {
|
|||
}
|
||||
}
|
||||
|
||||
if (roomId !== RoomViewStore.getRoomId()) {
|
||||
if (roomId !== RoomViewStore.instance.getRoomId()) {
|
||||
sendError(event, _t('Room %(roomId)s not visible', { roomId: roomId }));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { ComponentType } from "react";
|
||||
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { _t } from './languageHandler';
|
||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||
|
@ -34,6 +33,7 @@ import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreK
|
|||
import SettingsStore from "./settings/SettingsStore";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
|
@ -360,8 +360,6 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
|||
throw new Error("Secret storage creation canceled");
|
||||
}
|
||||
} else {
|
||||
// FIXME: Using an import will result in test failures
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
|
|
121
src/Skinner.ts
121
src/Skinner.ts
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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";
|
||||
|
||||
export interface IComponents {
|
||||
[key: string]: React.Component;
|
||||
}
|
||||
|
||||
export interface ISkinObject {
|
||||
components: IComponents;
|
||||
}
|
||||
|
||||
export class Skinner {
|
||||
public components: IComponents = null;
|
||||
|
||||
public getComponent(name: string): React.Component {
|
||||
if (!name) throw new Error(`Invalid component name: ${name}`);
|
||||
if (this.components === null) {
|
||||
throw new Error(
|
||||
`Attempted to get a component (${name}) before a skin has been loaded.`+
|
||||
" This is probably because either:"+
|
||||
" a) Your app has not called sdk.loadSkin(), or"+
|
||||
" b) A component has called getComponent at the root level",
|
||||
);
|
||||
}
|
||||
|
||||
const doLookup = (components: IComponents): React.Component => {
|
||||
if (!components) return null;
|
||||
let comp = components[name];
|
||||
// XXX: Temporarily also try 'views.' as we're currently
|
||||
// leaving the 'views.' off views.
|
||||
if (!comp) {
|
||||
comp = components['views.' + name];
|
||||
}
|
||||
return comp;
|
||||
};
|
||||
|
||||
// Check the skin first
|
||||
const comp = doLookup(this.components);
|
||||
|
||||
// Just return nothing instead of erroring - the consumer should be smart enough to
|
||||
// handle this at this point.
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// components have to be functions or forwardRef objects with a render function.
|
||||
const validType = typeof comp === 'function' || comp.render;
|
||||
if (!validType) {
|
||||
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
|
||||
public load(skinObject: ISkinObject): void {
|
||||
if (this.components !== null) {
|
||||
throw new Error(
|
||||
"Attempted to load a skin while a skin is already loaded"+
|
||||
"If you want to change the active skin, call resetSkin first");
|
||||
}
|
||||
this.components = {};
|
||||
const compKeys = Object.keys(skinObject.components);
|
||||
for (let i = 0; i < compKeys.length; ++i) {
|
||||
const comp = skinObject.components[compKeys[i]];
|
||||
this.addComponent(compKeys[i], comp);
|
||||
}
|
||||
|
||||
// Now that we have a skin, load our components too
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const idx = require("./component-index");
|
||||
if (!idx || !idx.components) throw new Error("Invalid react-sdk component index");
|
||||
for (const c in idx.components) {
|
||||
if (!this.components[c]) this.components[c] = idx.components[c];
|
||||
}
|
||||
}
|
||||
|
||||
public addComponent(name: string, comp: any) {
|
||||
let slot = name;
|
||||
if (comp.replaces !== undefined) {
|
||||
if (comp.replaces.indexOf('.') > -1) {
|
||||
slot = comp.replaces;
|
||||
} else {
|
||||
slot = name.substr(0, name.lastIndexOf('.') + 1) + comp.replaces.split('.').pop();
|
||||
}
|
||||
}
|
||||
this.components[slot] = comp;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.components = null;
|
||||
}
|
||||
}
|
||||
|
||||
// We define one Skinner globally, because the intention is
|
||||
// very much that it is a singleton. Relying on there only being one
|
||||
// copy of the module can be dicey and not work as browserify's
|
||||
// behaviour with multiple copies of files etc. is erratic at best.
|
||||
// XXX: We can still end up with the same file twice in the resulting
|
||||
// JS bundle which is nonideal.
|
||||
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
|
||||
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
|
||||
// ("Modules are cached based on their resolved filename")
|
||||
if (window.mxSkinner === undefined) {
|
||||
window.mxSkinner = new Skinner();
|
||||
}
|
||||
export default window.mxSkinner;
|
||||
|
|
@ -46,7 +46,7 @@ import BugReportDialog from "./components/views/dialogs/BugReportDialog";
|
|||
import { ensureDMExists } from "./createRoom";
|
||||
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
||||
|
@ -61,11 +61,12 @@ import InfoDialog from "./components/views/dialogs/InfoDialog";
|
|||
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
|
||||
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
|
||||
import { TimelineRenderingType } from './contexts/RoomContext';
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { RoomViewStore } from "./stores/RoomViewStore";
|
||||
import { XOR } from "./@types/common";
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||
import VoipUserMapper from './VoipUserMapper';
|
||||
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
|
@ -320,8 +321,8 @@ export const Commands = [
|
|||
}),
|
||||
new Command({
|
||||
command: 'jumptodate',
|
||||
args: '<date>',
|
||||
description: _td('Jump to the given date in the timeline (YYYY-MM-DD)'),
|
||||
args: '<YYYY-MM-DD>',
|
||||
description: _td('Jump to the given date in the timeline'),
|
||||
isEnabled: () => SettingsStore.getValue("feature_jump_to_date"),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
|
@ -851,7 +852,7 @@ export const Commands = [
|
|||
description: _td('Define the power level of a user'),
|
||||
isEnabled(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(RoomViewStore.getRoomId());
|
||||
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
|
||||
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId());
|
||||
},
|
||||
runFn: function(roomId, args) {
|
||||
|
@ -891,7 +892,7 @@ export const Commands = [
|
|||
description: _td('Deops user with given id'),
|
||||
isEnabled(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(RoomViewStore.getRoomId());
|
||||
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
|
||||
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId());
|
||||
},
|
||||
runFn: function(roomId, args) {
|
||||
|
|
|
@ -19,8 +19,8 @@ import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import * as sdk from '.';
|
||||
import Modal from './Modal';
|
||||
import TermsDialog from "./components/views/dialogs/TermsDialog";
|
||||
|
||||
export class TermsNotSignedError extends Error {}
|
||||
|
||||
|
@ -189,8 +189,6 @@ export async function dialogTermsInteractionCallback(
|
|||
extraClassNames?: string,
|
||||
): Promise<string[]> {
|
||||
logger.log("Terms that need agreement", policiesAndServicePairs);
|
||||
// FIXME: Using an import will result in test failures
|
||||
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
|
||||
|
||||
const { finished } = Modal.createTrackedDialog<[boolean, string[]]>('Terms of Service', '', TermsDialog, {
|
||||
policiesAndServicePairs,
|
||||
|
|
|
@ -317,16 +317,7 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
|||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = ev.getContent().body;
|
||||
if (ev.isRedacted()) {
|
||||
message = _t("Message deleted");
|
||||
const unsigned = ev.getUnsigned();
|
||||
const redactedBecauseUserId = unsigned?.redacted_because?.sender;
|
||||
if (redactedBecauseUserId && redactedBecauseUserId !== ev.getSender()) {
|
||||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
const sender = room?.getMember(redactedBecauseUserId);
|
||||
message = _t("Message deleted by %(name)s", {
|
||||
name: sender?.name || redactedBecauseUserId,
|
||||
});
|
||||
}
|
||||
message = textForRedactedPollAndMessageEvent(ev);
|
||||
}
|
||||
|
||||
if (SettingsStore.isEnabled("feature_extensible_events")) {
|
||||
|
@ -727,11 +718,38 @@ export function textForLocationEvent(event: MatrixEvent): () => string | null {
|
|||
});
|
||||
}
|
||||
|
||||
function textForRedactedPollAndMessageEvent(ev: MatrixEvent): string {
|
||||
let message = _t("Message deleted");
|
||||
const unsigned = ev.getUnsigned();
|
||||
const redactedBecauseUserId = unsigned?.redacted_because?.sender;
|
||||
if (redactedBecauseUserId && redactedBecauseUserId !== ev.getSender()) {
|
||||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
const sender = room?.getMember(redactedBecauseUserId);
|
||||
message = _t("Message deleted by %(name)s", {
|
||||
name: sender?.name || redactedBecauseUserId,
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function textForPollStartEvent(event: MatrixEvent): () => string | null {
|
||||
return () => _t("%(senderName)s has started a poll - %(pollQuestion)s", {
|
||||
senderName: getSenderName(event),
|
||||
pollQuestion: (event.unstableExtensibleEvent as PollStartEvent)?.question?.text,
|
||||
});
|
||||
return () => {
|
||||
let message = '';
|
||||
|
||||
if (event.isRedacted()) {
|
||||
message = textForRedactedPollAndMessageEvent(event);
|
||||
const senderDisplayName = event.sender?.name ?? event.getSender();
|
||||
message = senderDisplayName + ': ' + message;
|
||||
} else {
|
||||
message = _t("%(senderName)s has started a poll - %(pollQuestion)s", {
|
||||
senderName: getSenderName(event),
|
||||
pollQuestion: (event.unstableExtensibleEvent as PollStartEvent)?.question?.text,
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
}
|
||||
|
||||
function textForPollEndEvent(event: MatrixEvent): () => string | null {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import shouldHideEvent from './shouldHideEvent';
|
||||
import { haveTileForEvent } from "./components/views/rooms/EventTile";
|
||||
import { haveRendererForEvent } from "./events/EventTileFactory";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { RoomNotificationStateStore } from "./stores/notifications/RoomNotificationStateStore";
|
||||
|
||||
|
@ -48,7 +48,7 @@ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
|||
}
|
||||
|
||||
if (ev.isRedacted()) return false;
|
||||
return haveTileForEvent(ev);
|
||||
return haveRendererForEvent(ev, false /* hidden messages should never trigger unread counts anyways */);
|
||||
}
|
||||
|
||||
export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||
|
|
|
@ -18,10 +18,12 @@ import { Room } from 'matrix-js-sdk/src/models/room';
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { ensureVirtualRoomExists, findDMForUser } from './createRoom';
|
||||
import { ensureVirtualRoomExists } from './createRoom';
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler';
|
||||
import CallHandler from './CallHandler';
|
||||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
|
||||
import { findDMForUser } from "./utils/direct-messages";
|
||||
|
||||
// Functions for mapping virtual users & rooms. Currently the only lookup
|
||||
// is sip virtual: there could be others in the future.
|
||||
|
|
|
@ -25,7 +25,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
|
|||
import { arrayFastClone } from "../utils/arrays";
|
||||
import { PlaybackManager } from "./PlaybackManager";
|
||||
import { isVoiceMessage } from "../utils/EventUtils";
|
||||
import RoomViewStore from "../stores/RoomViewStore";
|
||||
import { RoomViewStore } from "../stores/RoomViewStore";
|
||||
|
||||
/**
|
||||
* Audio playback queue management for a given room. This keeps track of where the user
|
||||
|
@ -51,15 +51,15 @@ export class PlaybackQueue {
|
|||
constructor(private room: Room) {
|
||||
this.loadClocks();
|
||||
|
||||
RoomViewStore.addListener(() => {
|
||||
if (RoomViewStore.getRoomId() === this.room.roomId) {
|
||||
// Reset the state of the playbacks before they start mounting and enqueuing updates.
|
||||
// We reset the entirety of the queue, including order, to ensure the user isn't left
|
||||
// confused with what order the messages are playing in.
|
||||
this.currentPlaybackId = null; // this in particular stops autoplay when the room is switched to
|
||||
this.recentFullPlays = new Set<string>();
|
||||
this.playbackIdOrder = [];
|
||||
}
|
||||
RoomViewStore.instance.addRoomListener(this.room.roomId, (isActive) => {
|
||||
if (!isActive) return;
|
||||
|
||||
// Reset the state of the playbacks before they start mounting and enqueuing updates.
|
||||
// We reset the entirety of the queue, including order, to ensure the user isn't left
|
||||
// confused with what order the messages are playing in.
|
||||
this.currentPlaybackId = null; // this in particular stops autoplay when the room is switched to
|
||||
this.recentFullPlays = new Set<string>();
|
||||
this.playbackIdOrder = [];
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
// check if the full match differs from the first word (i.e. returns false if the command has args)
|
||||
if (command[0] !== command[1]) {
|
||||
// The input looks like a command with arguments, perform exact match
|
||||
const name = command[1].substr(1); // strip leading `/`
|
||||
const name = command[1].slice(1); // strip leading `/`
|
||||
if (CommandMap.has(name) && CommandMap.get(name).isEnabled()) {
|
||||
// some commands, namely `me` don't suit having the usage shown whilst typing their arguments
|
||||
if (CommandMap.get(name).hideCompletionAfterSpace) return [];
|
||||
|
|
|
@ -3,6 +3,7 @@ Copyright 2016 Aviral Dasgupta
|
|||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 Ryan Browne <code@commonlawfeature.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -62,6 +63,13 @@ function score(query, space) {
|
|||
}
|
||||
}
|
||||
|
||||
function colonsTrimmed(str: string): string {
|
||||
// Trim off leading and potentially trailing `:` to correctly match the emoji data as they exist in emojibase.
|
||||
// Notes: The regex is pinned to the start and end of the string so that we can use the lazy-capturing `*?` matcher.
|
||||
// It needs to be lazy so that the trailing `:` is not captured in the replacement group, if it exists.
|
||||
return str.replace(/^:(.*?):?$/, "$1");
|
||||
}
|
||||
|
||||
export default class EmojiProvider extends AutocompleteProvider {
|
||||
matcher: QueryMatcher<ISortedEmoji>;
|
||||
nameMatcher: QueryMatcher<ISortedEmoji>;
|
||||
|
@ -108,8 +116,9 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
// then sort by score (Infinity if matchedString not in shortcode)
|
||||
sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
|
||||
// then sort by max score of all shortcodes, trim off the `:`
|
||||
const trimmedMatch = colonsTrimmed(matchedString);
|
||||
sorters.push(c => Math.min(
|
||||
...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
|
||||
...c.emoji.shortcodes.map(s => score(trimmedMatch, s)),
|
||||
));
|
||||
// If the matchedString is not empty, sort by length of shortcode. Example:
|
||||
// matchedString = ":bookmark"
|
||||
|
|
19
src/call-types.ts
Normal file
19
src/call-types.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
// Event type for room account data and room creation content used to mark rooms as virtual rooms
|
||||
// (and store the ID of their native room)
|
||||
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
|
@ -22,7 +22,6 @@ import classNames from "classnames";
|
|||
import FocusLock from "react-focus-lock";
|
||||
|
||||
import { Writeable } from "../../@types/common";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
|
@ -105,7 +104,6 @@ interface IState {
|
|||
// Generic ContextMenu Portal wrapper
|
||||
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||
@replaceableComponent("structures.ContextMenu")
|
||||
export default class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||
private readonly initialFocus: HTMLElement;
|
||||
|
||||
|
@ -431,7 +429,7 @@ export type AboveLeftOf = IPosition & {
|
|||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||
export const aboveLeftOf = (
|
||||
elementRect: DOMRect,
|
||||
elementRect: Pick<DOMRect, "right" | "top" | "bottom">,
|
||||
chevronFace = ChevronFace.None,
|
||||
vPadding = 0,
|
||||
): AboveLeftOf => {
|
||||
|
@ -452,9 +450,37 @@ export const aboveLeftOf = (
|
|||
return menuOptions;
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the right of elementRect,
|
||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowRightOf?)
|
||||
export const aboveRightOf = (
|
||||
elementRect: Pick<DOMRect, "left" | "top" | "bottom">,
|
||||
chevronFace = ChevronFace.None,
|
||||
vPadding = 0,
|
||||
): AboveLeftOf => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonLeft = elementRect.left + window.pageXOffset;
|
||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the left edge of the menu to the left edge of the button
|
||||
menuOptions.left = buttonLeft;
|
||||
// Align the menu vertically on whichever side of the button has more space available.
|
||||
if (buttonBottom < UIStore.instance.windowHeight / 2) {
|
||||
menuOptions.top = buttonBottom + vPadding;
|
||||
} else {
|
||||
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||
}
|
||||
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
// and always above elementRect
|
||||
export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
export const alwaysAboveLeftOf = (
|
||||
elementRect: Pick<DOMRect, "right" | "bottom" | "top">,
|
||||
chevronFace = ChevronFace.None,
|
||||
vPadding = 0,
|
||||
) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonRight = elementRect.right + window.pageXOffset;
|
||||
|
@ -474,7 +500,11 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
|
|||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the right of elementRect
|
||||
// and always above elementRect
|
||||
export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
export const alwaysAboveRightOf = (
|
||||
elementRect: Pick<DOMRect, "left" | "top">,
|
||||
chevronFace = ChevronFace.None,
|
||||
vPadding = 0,
|
||||
) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonLeft = elementRect.left + window.pageXOffset;
|
||||
|
|
|
@ -37,7 +37,7 @@ interface IProps {
|
|||
// Whether to wrap the page in a scrollbar
|
||||
scrollbar?: boolean;
|
||||
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
||||
replaceMap?: Map<string, string>;
|
||||
replaceMap?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -57,8 +57,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
}
|
||||
|
||||
protected translate(s: string): string {
|
||||
// default implementation - skins may wish to extend this
|
||||
private translate(s: string): string {
|
||||
return sanitizeHtml(_t(s));
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import EventIndexPeg from "../../indexing/EventIndexPeg";
|
|||
import { _t } from '../../languageHandler';
|
||||
import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
|
@ -51,7 +50,6 @@ interface IState {
|
|||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
*/
|
||||
@replaceableComponent("structures.FilePanel")
|
||||
class FilePanel extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
|
|
|
@ -16,14 +16,11 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
title: React.ReactNode;
|
||||
message: React.ReactNode;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.GenericErrorPage")
|
||||
export default class GenericErrorPage extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
return <div className='mx_GenericErrorPage'>
|
||||
|
|
|
@ -21,7 +21,6 @@ import AutoHideScrollbar from './AutoHideScrollbar';
|
|||
import { getHomePageUrl } from "../../utils/pages";
|
||||
import { _tDom } from "../../languageHandler";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import * as sdk from "../../index";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
|
@ -33,6 +32,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
|||
import MiniAvatarUploader, { AVATAR_SIZE } from "../views/elements/MiniAvatarUploader";
|
||||
import Analytics from "../../Analytics";
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import EmbeddedPage from "./EmbeddedPage";
|
||||
|
||||
const onClickSendDm = () => {
|
||||
Analytics.trackEvent('home_page', 'button', 'dm');
|
||||
|
@ -94,8 +94,6 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
|
|||
const pageUrl = getHomePageUrl(config);
|
||||
|
||||
if (pageUrl) {
|
||||
// FIXME: Using an import will result in wrench-element-tests failures
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
import { _t } from "../../languageHandler";
|
||||
import { HostSignupStore } from "../../stores/HostSignupStore";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onClick?(): void;
|
||||
|
@ -31,7 +30,6 @@ interface IProps {
|
|||
|
||||
interface IState {}
|
||||
|
||||
@replaceableComponent("structures.HostSignupAction")
|
||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||
private openDialog = async () => {
|
||||
this.props.onClick?.();
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React, { ComponentProps, createRef } from "react";
|
||||
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel"> {
|
||||
|
@ -40,7 +39,6 @@ interface IState {
|
|||
rightIndicatorOffset: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.IndicatorScrollbar")
|
||||
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
|
||||
private autoHideScrollbar = createRef<AutoHideScrollbar>();
|
||||
private scrollElement: HTMLDivElement;
|
||||
|
|
|
@ -28,7 +28,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import getEntryComponentForLoginType, { IStageComponent } from '../views/auth/InteractiveAuthEntryComponents';
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
|
||||
|
||||
|
@ -89,7 +88,6 @@ interface IState {
|
|||
submitButtonEnabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.InteractiveAuthComponent")
|
||||
export default class InteractiveAuthComponent extends React.Component<IProps, IState> {
|
||||
private readonly authLogic: InteractiveAuth;
|
||||
private readonly intervalId: number = null;
|
||||
|
|
|
@ -27,7 +27,6 @@ import { Action } from "../../dispatcher/actions";
|
|||
import RoomSearch from "./RoomSearch";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
|
@ -61,7 +60,6 @@ interface IState {
|
|||
activeSpace: SpaceKey;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.LeftPanel")
|
||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
private listContainerRef = createRef<HTMLDivElement>();
|
||||
private roomSearchRef = createRef<RoomSearch>();
|
||||
|
|
|
@ -52,7 +52,6 @@ import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
|||
import { getKeyBindingsManager } from '../../KeyBindingsManager';
|
||||
import { IOpts } from "../../createRoom";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
||||
|
@ -63,7 +62,7 @@ import ToastContainer from './ToastContainer';
|
|||
import UserView from "./UserView";
|
||||
import BackdropPanel from "./BackdropPanel";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import { UserTab } from "../views/dialogs/UserTab";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import RightPanelStore from '../../stores/right-panel/RightPanelStore';
|
||||
import { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
|
@ -127,7 +126,6 @@ interface IState {
|
|||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
*/
|
||||
@replaceableComponent("structures.LoggedInView")
|
||||
class LoggedInView extends React.Component<IProps, IState> {
|
||||
static displayName = 'LoggedInView';
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import React from 'react';
|
|||
import { NumberSize, Resizable } from 're-resizable';
|
||||
import { Direction } from "re-resizable/lib/resizer";
|
||||
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
|
||||
interface IProps {
|
||||
|
@ -28,7 +27,6 @@ interface IProps {
|
|||
panel?: JSX.Element;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.MainSplit")
|
||||
export default class MainSplit extends React.Component<IProps> {
|
||||
private onResizeStart = (): void => {
|
||||
this.props.resizeNotifier.startResizing();
|
||||
|
|
|
@ -33,7 +33,7 @@ import { throttle } from "lodash";
|
|||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by various components
|
||||
import 'focus-visible';
|
||||
// what-input helps improve keyboard accessibility
|
||||
import 'what-input';
|
||||
|
@ -84,19 +84,18 @@ import {
|
|||
UPDATE_STATUS_INDICATOR,
|
||||
} from "../../stores/notifications/RoomNotificationStateStore";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { leaveRoomBehaviour } from "../../utils/membership";
|
||||
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
|
||||
import { UIFeature } from "../../settings/UIFeature";
|
||||
import DialPadModal from "../views/voip/DialPadModal";
|
||||
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
|
||||
import { shouldUseLoginForWelcome } from "../../utils/pages";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { RoomUpdateCause } from "../../stores/room-list/models";
|
||||
import SecurityCustomisations from "../../customisations/Security";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||
import UserSettingsDialog, { UserTab } from '../views/dialogs/UserSettingsDialog';
|
||||
import UserSettingsDialog from '../views/dialogs/UserSettingsDialog';
|
||||
import { UserTab } from "../views/dialogs/UserTab";
|
||||
import CreateRoomDialog from '../views/dialogs/CreateRoomDialog';
|
||||
import RoomDirectory from './RoomDirectory';
|
||||
import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog";
|
||||
|
@ -131,6 +130,7 @@ import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStart
|
|||
import { IConfigOptions } from "../../IConfigOptions";
|
||||
import { SnakedObject } from "../../utils/SnakedObject";
|
||||
import InfoDialog from '../views/dialogs/InfoDialog';
|
||||
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
|
@ -208,7 +208,6 @@ interface IState {
|
|||
forceTimeline?: boolean; // see props
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.MatrixChat")
|
||||
export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
static displayName = "MatrixChat";
|
||||
|
||||
|
@ -2097,12 +2096,3 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
</ErrorBoundary>;
|
||||
}
|
||||
}
|
||||
|
||||
export function isLoggedIn(): boolean {
|
||||
// JRS: Maybe we should move the step that writes this to the window out of
|
||||
// `element-web` and into this file? Better yet, we should probably create a
|
||||
// store to hold this state.
|
||||
// See also https://github.com/vector-im/element-web/issues/15034.
|
||||
const app = window.matrixChat;
|
||||
return app && (app as MatrixChat).state.view === Views.LOGGED_IN;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
|
||||
import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
@ -31,13 +31,12 @@ import SettingsStore from '../../settings/SettingsStore';
|
|||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import { _t } from "../../languageHandler";
|
||||
import EventTile, { UnwrappedEventTile, haveTileForEvent, IReadReceiptProps } from "../views/rooms/EventTile";
|
||||
import EventTile, { UnwrappedEventTile, IReadReceiptProps } from "../views/rooms/EventTile";
|
||||
import { hasText } from "../../TextForEvent";
|
||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||
import DMRoomMap from "../../utils/DMRoomMap";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
import HistoryTile from "../views/rooms/HistoryTile";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||
import CallEventGrouper from "./CallEventGrouper";
|
||||
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
||||
|
@ -51,8 +50,9 @@ import Spinner from "../views/elements/Spinner";
|
|||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
import { getEventDisplayInfo } from "../../utils/EventUtils";
|
||||
import { getEventDisplayInfo } from "../../utils/EventRenderingUtils";
|
||||
import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
|
||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||
import { editorRoomKey } from "../../Editing";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
|
@ -97,7 +97,7 @@ export function shouldFormContinuation(
|
|||
timelineRenderingType !== TimelineRenderingType.Thread) return false;
|
||||
|
||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||
if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
|
||||
if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -170,9 +170,6 @@ interface IProps {
|
|||
// callback which is called when the panel is scrolled.
|
||||
onScroll?(event: Event): void;
|
||||
|
||||
// callback which is called when the user interacts with the room timeline
|
||||
onUserScroll(event: SyntheticEvent): void;
|
||||
|
||||
// callback which is called when more content is needed.
|
||||
onFillRequest?(backwards: boolean): Promise<boolean>;
|
||||
|
||||
|
@ -200,7 +197,6 @@ interface IReadReceiptForUser {
|
|||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
@replaceableComponent("structures.MessagePanel")
|
||||
export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
@ -246,7 +242,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
// displayed event in the current render cycle.
|
||||
private readReceiptsByUserId: Record<string, IReadReceiptForUser> = {};
|
||||
|
||||
private readonly showHiddenEventsInTimeline: boolean;
|
||||
private readonly _showHiddenEvents: boolean;
|
||||
private readonly threadsEnabled: boolean;
|
||||
private isMounted = false;
|
||||
|
||||
|
@ -274,7 +270,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
// Cache these settings on mount since Settings is expensive to query,
|
||||
// and we check this in a hot code path. This is also cached in our
|
||||
// RoomContext, however we still need a fallback for roomless MessagePanels.
|
||||
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||
this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||
this.threadsEnabled = SettingsStore.getValue("feature_thread");
|
||||
|
||||
this.showTypingNotificationsWatcherRef =
|
||||
|
@ -469,7 +465,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public get showHiddenEvents(): boolean {
|
||||
return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
|
||||
return this.context?.showHiddenEvents ?? this._showHiddenEvents;
|
||||
}
|
||||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
|
@ -492,7 +488,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
|
||||
if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) {
|
||||
return false; // no tile = no show
|
||||
}
|
||||
|
||||
|
@ -752,7 +748,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date());
|
||||
lastInSection = willWantDateSeparator ||
|
||||
mxEv.getSender() !== nextEv.getSender() ||
|
||||
getEventDisplayInfo(nextEv).isInfoMessage ||
|
||||
getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage ||
|
||||
!shouldFormContinuation(
|
||||
mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType,
|
||||
);
|
||||
|
@ -1031,7 +1027,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
ref={this.scrollPanel}
|
||||
className={classes}
|
||||
onScroll={this.props.onScroll}
|
||||
onUserScroll={this.props.onUserScroll}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
|
|
|
@ -19,7 +19,6 @@ import * as React from "react";
|
|||
import { ComponentClass } from "../../@types/common";
|
||||
import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -28,7 +27,6 @@ interface IState {
|
|||
toasts: ComponentClass[];
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.NonUrgentToastContainer")
|
||||
export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props, context) {
|
||||
super(props, context);
|
||||
|
|
|
@ -20,7 +20,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { _t } from '../../languageHandler';
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
|
@ -38,7 +37,6 @@ interface IState {
|
|||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
*/
|
||||
@replaceableComponent("structures.NotificationPanel")
|
||||
export default class NotificationPanel extends React.PureComponent<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
|||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import MemberList from "../views/rooms/MemberList";
|
||||
import UserInfo from "../views/right_panel/UserInfo";
|
||||
|
@ -60,7 +59,6 @@ interface IState {
|
|||
cardState?: IRightPanelCardState;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RightPanel")
|
||||
export default class RightPanel extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
@ -233,6 +231,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
mxEvent={cardState.threadHeadEvent}
|
||||
initialEvent={cardState.initialEvent}
|
||||
isInitialEventHighlighted={cardState.isInitialEventHighlighted}
|
||||
initialEventScrollIntoView={cardState.initialEventScrollIntoView}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
/>;
|
||||
|
|
|
@ -24,18 +24,14 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import { _t } from '../../languageHandler';
|
||||
import SdkConfig from '../../SdkConfig';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
|
@ -46,9 +42,7 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
|
|||
import { Action } from "../../dispatcher/actions";
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 800;
|
||||
import { PublicRoomTile } from "../views/rooms/PublicRoomTile";
|
||||
|
||||
const LAST_SERVER_KEY = "mx_last_room_directory_server";
|
||||
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
|
||||
|
@ -71,7 +65,6 @@ interface IState {
|
|||
filterString: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomDirectory")
|
||||
export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private nextBatch: string = null;
|
||||
|
@ -251,7 +244,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
* HS admins to do this through the RoomSettings interface, but
|
||||
* this needs SPEC-417.
|
||||
*/
|
||||
private removeFromDirectory(room: IPublicRoomsChunkRoom) {
|
||||
private removeFromDirectory = (room: IPublicRoomsChunkRoom) => {
|
||||
const alias = getDisplayAliasForRoom(room);
|
||||
const name = room.name || alias || _t('Unnamed room');
|
||||
|
||||
|
@ -291,14 +284,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: React.MouseEvent) => {
|
||||
// If room was shift-clicked, remove it from the room directory
|
||||
if (ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
this.removeFromDirectory(room);
|
||||
}
|
||||
};
|
||||
|
||||
private onOptionChange = (server: string, instanceId?: string) => {
|
||||
|
@ -406,21 +391,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onPreviewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
|
||||
this.showRoom(room, null, false, true);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private onViewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
|
||||
this.showRoom(room);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private onJoinClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
|
||||
this.showRoom(room, null, true);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private onCreateRoomClick = (ev: ButtonEvent) => {
|
||||
this.onFinished();
|
||||
dis.dispatch({
|
||||
|
@ -435,7 +405,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
this.showRoom(null, alias, autoJoin);
|
||||
}
|
||||
|
||||
private showRoom(room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
|
||||
private showRoom = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => {
|
||||
this.onFinished();
|
||||
const payload: ViewRoomPayload = {
|
||||
action: Action.ViewRoom,
|
||||
|
@ -479,112 +449,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
payload.room_id = room.room_id;
|
||||
}
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
|
||||
private createRoomCells(room: IPublicRoomsChunkRoom) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const clientRoom = client.getRoom(room.room_id);
|
||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||
const isGuest = client.isGuest();
|
||||
let previewButton;
|
||||
let joinOrViewButton;
|
||||
|
||||
// Element Web currently does not allow guests to join rooms, so we
|
||||
// instead show them preview buttons for all rooms. If the room is not
|
||||
// world readable, a modal will appear asking you to register first. If
|
||||
// it is readable, the preview appears as normal.
|
||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||
previewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>
|
||||
{ _t("Preview") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (hasJoinedRoom) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>
|
||||
{ _t("View") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (!isGuest) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
|
||||
if (name.length > MAX_NAME_LENGTH) {
|
||||
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
||||
}
|
||||
|
||||
let topic = room.topic || '';
|
||||
// Additional truncation based on line numbers is done via CSS,
|
||||
// but to ensure that the DOM is not polluted with a huge string
|
||||
// we give it a hard limit before rendering.
|
||||
if (topic.length > MAX_TOPIC_LENGTH) {
|
||||
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
|
||||
}
|
||||
topic = linkifyAndSanitizeHtml(topic);
|
||||
let avatarUrl = null;
|
||||
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
||||
|
||||
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
|
||||
return <div
|
||||
key={room.room_id}
|
||||
role="listitem"
|
||||
className="mx_RoomDirectory_listItem"
|
||||
>
|
||||
<div
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<BaseAvatar
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod='crop'
|
||||
name={name}
|
||||
idName={name}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
<div className="mx_RoomDirectory_name">
|
||||
{ name }
|
||||
</div>
|
||||
<div
|
||||
className="mx_RoomDirectory_topic"
|
||||
dangerouslySetInnerHTML={{ __html: topic }}
|
||||
/>
|
||||
<div className="mx_RoomDirectory_alias">
|
||||
{ getDisplayAliasForRoom(room) }
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>
|
||||
<div
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_preview"
|
||||
>
|
||||
{ previewButton }
|
||||
</div>
|
||||
<div
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
{ joinOrViewButton }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
};
|
||||
private stringLooksLikeId(s: string, fieldType: IFieldType) {
|
||||
let pat = /^#[^\s]+:[^\s]/;
|
||||
if (fieldType && fieldType.regexp) {
|
||||
|
@ -622,7 +487,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
content = <Spinner />;
|
||||
} else {
|
||||
const cells = (this.state.publicRooms || [])
|
||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
|
||||
.map(room =>
|
||||
<PublicRoomTile
|
||||
key={room.room_id}
|
||||
room={room}
|
||||
showRoom={this.showRoom}
|
||||
removeFromDirectory={this.removeFromDirectory}
|
||||
/>,
|
||||
);
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
|
|
|
@ -26,7 +26,6 @@ import { Action } from "../../dispatcher/actions";
|
|||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { isMac, Key } from "../../Keyboard";
|
||||
|
@ -50,7 +49,6 @@ interface IState {
|
|||
spotlightBetaEnabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomSearch")
|
||||
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly betaRef: string;
|
||||
|
|
|
@ -24,7 +24,6 @@ import Resend from '../../Resend';
|
|||
import dis from '../../dispatcher/dispatcher';
|
||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
|
@ -82,7 +81,6 @@ interface IState {
|
|||
isResending: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomStatusBar")
|
||||
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
public static contextType = MatrixClientContext;
|
||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
|||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { IRecommendedVersion, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -35,6 +35,7 @@ import { throttle } from "lodash";
|
|||
import { MatrixError } from 'matrix-js-sdk/src/http-api';
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
@ -48,14 +49,13 @@ import * as Rooms from '../../Rooms';
|
|||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import { RoomViewStore } from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
|
@ -86,7 +86,6 @@ import { getKeyBindingsManager } from '../../KeyBindingsManager';
|
|||
import { objectHasDiff } from "../../utils/objects";
|
||||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
||||
import SearchResultTile from '../views/rooms/SearchResultTile';
|
||||
|
@ -109,6 +108,7 @@ import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyn
|
|||
import FileDropTarget from './FileDropTarget';
|
||||
import Measured from '../views/elements/Measured';
|
||||
import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload';
|
||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -156,6 +156,8 @@ export interface IRoomState {
|
|||
initialEventPixelOffset?: number;
|
||||
// Whether to highlight the event scrolled to
|
||||
isInitialEventHighlighted?: boolean;
|
||||
// Whether to scroll the event into view
|
||||
initialEventScrollIntoView?: boolean;
|
||||
replyToEvent?: MatrixEvent;
|
||||
numUnreadMessages: number;
|
||||
searchTerm?: string;
|
||||
|
@ -197,7 +199,7 @@ export interface IRoomState {
|
|||
showTwelveHourTimestamps: boolean;
|
||||
readMarkerInViewThresholdMs: number;
|
||||
readMarkerOutOfViewThresholdMs: number;
|
||||
showHiddenEventsInTimeline: boolean;
|
||||
showHiddenEvents: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRedactions: boolean;
|
||||
showJoinLeaves: boolean;
|
||||
|
@ -220,7 +222,6 @@ export interface IRoomState {
|
|||
narrow: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
|
@ -270,7 +271,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
|
||||
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
|
||||
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||
showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||
showReadReceipts: true,
|
||||
showRedactions: true,
|
||||
showJoinLeaves: true,
|
||||
|
@ -298,7 +299,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
context.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
|
||||
context.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
// Start listening for RoomViewStore updates
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.roomStoreToken = RoomViewStore.instance.addListener(this.onRoomViewStoreUpdate);
|
||||
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
|
||||
|
@ -327,7 +328,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
|
||||
),
|
||||
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[,,, value]) =>
|
||||
this.setState({ showHiddenEventsInTimeline: value as boolean }),
|
||||
this.setState({ showHiddenEvents: value as boolean }),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -389,7 +390,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
|
||||
if (!initial && this.state.roomId !== RoomViewStore.instance.getRoomId()) {
|
||||
// RoomView explicitly does not support changing what room
|
||||
// is being viewed: instead it should just be re-mounted when
|
||||
// switching rooms. Therefore, if the room ID changes, we
|
||||
|
@ -404,28 +405,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
const roomId = RoomViewStore.instance.getRoomId();
|
||||
|
||||
const newState: Pick<IRoomState, any> = {
|
||||
// This convoluted type signature ensures we get IntelliSense *and* correct typing
|
||||
const newState: Partial<IRoomState> & Pick<IRoomState, any> = {
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||
joining: RoomViewStore.isJoining(),
|
||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||
roomAlias: RoomViewStore.instance.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.instance.isRoomLoading(),
|
||||
roomLoadError: RoomViewStore.instance.getRoomLoadError(),
|
||||
joining: RoomViewStore.instance.isJoining(),
|
||||
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.instance.shouldPeek(),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||
wasContextSwitch: RoomViewStore.instance.getWasContextSwitch(),
|
||||
initialEventId: null, // default to clearing this, will get set later in the method if needed
|
||||
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
|
||||
};
|
||||
|
||||
const initialEventId = RoomViewStore.getInitialEventId();
|
||||
const initialEventId = RoomViewStore.instance.getInitialEventId();
|
||||
if (initialEventId) {
|
||||
const room = this.context.getRoom(roomId);
|
||||
let initialEvent = room?.findEventById(initialEventId);
|
||||
|
@ -445,22 +447,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
|
||||
// If we have an initial event, we want to reset the event pixel offset to ensure it ends up
|
||||
// visible
|
||||
newState.initialEventPixelOffset = null;
|
||||
|
||||
const thread = initialEvent?.getThread();
|
||||
if (thread && !initialEvent?.isThreadRoot) {
|
||||
showThread({
|
||||
rootEvent: thread.rootEvent,
|
||||
initialEvent,
|
||||
highlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
|
||||
});
|
||||
} else {
|
||||
newState.initialEventId = initialEventId;
|
||||
newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted();
|
||||
newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
|
||||
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
|
||||
|
||||
if (thread && initialEvent?.isThreadRoot) {
|
||||
showThread({
|
||||
rootEvent: thread.rootEvent,
|
||||
initialEvent,
|
||||
highlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -760,19 +769,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onUserScroll = () => {
|
||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.room.roomId,
|
||||
event_id: this.state.initialEventId,
|
||||
highlighted: false,
|
||||
replyingToEvent: this.state.replyToEvent,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
|
||||
|
@ -1303,6 +1299,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.updateTopUnreadMessagesBar();
|
||||
};
|
||||
|
||||
private resetJumpToEvent = (eventId?: string) => {
|
||||
if (this.state.initialEventId && this.state.initialEventScrollIntoView &&
|
||||
this.state.initialEventId === eventId) {
|
||||
debuglog("Removing scroll_into_view flag from initial event");
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.room.roomId,
|
||||
event_id: this.state.initialEventId,
|
||||
highlighted: this.state.isInitialEventHighlighted,
|
||||
scroll_into_view: false,
|
||||
replyingToEvent: this.state.replyToEvent,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
|
||||
if (this.context.isGuest()) {
|
||||
dis.dispatch({ action: 'require_registration' });
|
||||
|
@ -1347,7 +1359,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.handleSearchResult(searchPromise);
|
||||
};
|
||||
|
||||
private handleSearchResult(searchPromise: Promise<any>): Promise<boolean> {
|
||||
private handleSearchResult(searchPromise: Promise<ISearchResults>): Promise<boolean> {
|
||||
// keep a record of the current search id, so that if the search terms
|
||||
// change before we get a response, we can ignore the results.
|
||||
const localSearchId = this.searchId;
|
||||
|
@ -1356,7 +1368,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
searchInProgress: true,
|
||||
});
|
||||
|
||||
return searchPromise.then((results) => {
|
||||
return searchPromise.then(async (results) => {
|
||||
debuglog("search complete");
|
||||
if (this.unmounted ||
|
||||
this.state.timelineRenderingType !== TimelineRenderingType.Search ||
|
||||
|
@ -1383,6 +1395,20 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return b.length - a.length;
|
||||
});
|
||||
|
||||
if (this.context.supportsExperimentalThreads()) {
|
||||
// Process all thread roots returned in this batch of search results
|
||||
// XXX: This won't work for results coming from Seshat which won't include the bundled relationship
|
||||
for (const result of results.results) {
|
||||
for (const event of result.context.getTimeline()) {
|
||||
const bundledRelationship = event
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
if (!bundledRelationship || event.getThread()) continue;
|
||||
const room = this.context.getRoom(event.getRoomId());
|
||||
event.setThread(room.findThreadForEvent(event) ?? room.createThread(event, [], true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
searchHighlights: highlights,
|
||||
searchResults: results,
|
||||
|
@ -1454,7 +1480,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
|
||||
if (!haveRendererForEvent(mxEv, this.state.showHiddenEvents)) {
|
||||
// XXX: can this ever happen? It will make the result count
|
||||
// not match the displayed count.
|
||||
continue;
|
||||
|
@ -2053,9 +2079,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
hidden={hideMessagePanel}
|
||||
highlightedEventId={highlightedEventId}
|
||||
eventId={this.state.initialEventId}
|
||||
eventScrollIntoView={this.state.initialEventScrollIntoView}
|
||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onUserScroll={this.onUserScroll}
|
||||
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||
showUrlPreview={this.state.showUrlPreview}
|
||||
className={this.messagePanelClassNames}
|
||||
|
|
|
@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
|
||||
import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import Timer from '../../utils/Timer';
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
|
@ -110,10 +109,6 @@ interface IProps {
|
|||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll?(event: Event): void;
|
||||
|
||||
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
||||
*/
|
||||
onUserScroll?(event: SyntheticEvent): void;
|
||||
}
|
||||
|
||||
/* This component implements an intelligent scrolling list.
|
||||
|
@ -170,7 +165,6 @@ interface IPreventShrinkingState {
|
|||
offsetNode: HTMLElement;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ScrollPanel")
|
||||
export default class ScrollPanel extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
stickyBottom: true,
|
||||
|
@ -595,29 +589,21 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
* @param {object} ev the keyboard event
|
||||
*/
|
||||
public handleScrollKey = (ev: KeyboardEvent) => {
|
||||
let isScrolling = false;
|
||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||
switch (roomAction) {
|
||||
case KeyBindingAction.ScrollUp:
|
||||
this.scrollRelative(-1);
|
||||
isScrolling = true;
|
||||
break;
|
||||
case KeyBindingAction.ScrollDown:
|
||||
this.scrollRelative(1);
|
||||
isScrolling = true;
|
||||
break;
|
||||
case KeyBindingAction.JumpToFirstMessage:
|
||||
this.scrollToTop();
|
||||
isScrolling = true;
|
||||
break;
|
||||
case KeyBindingAction.JumpToLatestMessage:
|
||||
this.scrollToBottom();
|
||||
isScrolling = true;
|
||||
break;
|
||||
}
|
||||
if (isScrolling && this.props.onUserScroll) {
|
||||
this.props.onUserScroll(ev);
|
||||
}
|
||||
};
|
||||
|
||||
/* Scroll the panel to bring the DOM node with the scroll token
|
||||
|
@ -967,7 +953,6 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
<AutoHideScrollbar
|
||||
wrappedRef={this.collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
onWheel={this.props.onUserScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`}
|
||||
style={this.props.style}
|
||||
>
|
||||
|
|
|
@ -20,7 +20,6 @@ import { throttle } from 'lodash';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
|
||||
|
@ -43,7 +42,6 @@ interface IState {
|
|||
blurred: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.SearchBox")
|
||||
export default class SearchBox extends React.Component<IProps, IState> {
|
||||
private search = createRef<HTMLInputElement>();
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
|||
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
|
||||
import { IOOBData } from "../../stores/ThreepidInviteStore";
|
||||
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
|
||||
import RoomViewStore from "../../stores/RoomViewStore";
|
||||
import { RoomViewStore } from "../../stores/RoomViewStore";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
|
@ -371,7 +371,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
|
|||
metricsTrigger: "SpaceHierarchy",
|
||||
});
|
||||
}, err => {
|
||||
RoomViewStore.showJoinRoomError(err, roomId);
|
||||
RoomViewStore.instance.showJoinRoomError(err, roomId);
|
||||
});
|
||||
|
||||
return prom;
|
||||
|
|
|
@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { _t } from '../../languageHandler';
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers";
|
||||
|
||||
|
@ -64,7 +63,6 @@ interface IState {
|
|||
activeTabIndex: number;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.TabbedView")
|
||||
export default class TabbedView extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
|
@ -37,7 +37,7 @@ import SdkConfig from '../../SdkConfig';
|
|||
import Modal from '../../Modal';
|
||||
import BetaFeedbackDialog from '../views/dialogs/BetaFeedbackDialog';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
import { UserTab } from '../views/dialogs/UserSettingsDialog';
|
||||
import { UserTab } from '../views/dialogs/UserTab';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
|
||||
interface IProps {
|
||||
|
@ -163,7 +163,9 @@ const EmptyThread: React.FC<EmptyThreadIProps> = ({ hasThreads, filterOption, sh
|
|||
body = <>
|
||||
<p>{ _t("Threads help keep your conversations on-topic and easy to track.") }</p>
|
||||
<p className="mx_ThreadPanel_empty_tip">
|
||||
{ _t('<b>Tip:</b> Use "Reply in thread" when hovering over a message.', {}, {
|
||||
{ _t('<b>Tip:</b> Use “%(replyInThread)s” when hovering over a message.', {
|
||||
replyInThread: _t("Reply in thread"),
|
||||
}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }
|
||||
</p>
|
||||
|
@ -250,7 +252,7 @@ const ThreadPanel: React.FC<IProps> = ({
|
|||
<RoomContext.Provider value={{
|
||||
...roomContext,
|
||||
timelineRenderingType: TimelineRenderingType.ThreadsList,
|
||||
showHiddenEventsInTimeline: true,
|
||||
showHiddenEvents: true,
|
||||
narrow,
|
||||
}}>
|
||||
<BaseCard
|
||||
|
|
|
@ -25,7 +25,6 @@ import classNames from "classnames";
|
|||
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import MessageComposer from '../views/rooms/MessageComposer';
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
|
@ -51,7 +50,7 @@ import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
|||
import Measured from '../views/elements/Measured';
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import { RoomViewStore } from '../../stores/RoomViewStore';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -62,6 +61,7 @@ interface IProps {
|
|||
e2eStatus?: E2EStatus;
|
||||
initialEvent?: MatrixEvent;
|
||||
isInitialEventHighlighted?: boolean;
|
||||
initialEventScrollIntoView?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -73,7 +73,6 @@ interface IState {
|
|||
narrow: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
@ -112,7 +111,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
room.removeListener(ThreadEvent.New, this.onNewThread);
|
||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||
|
||||
const hasRoomChanged = RoomViewStore.getRoomId() !== roomId;
|
||||
const hasRoomChanged = RoomViewStore.instance.getRoomId() !== roomId;
|
||||
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -217,13 +216,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private resetHighlightedEvent = (): void => {
|
||||
if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
|
||||
private resetJumpToEvent = (event?: string): void => {
|
||||
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
|
||||
event === this.props.initialEvent?.getId()) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.props.room.roomId,
|
||||
event_id: this.props.initialEvent?.getId(),
|
||||
highlighted: false,
|
||||
highlighted: this.props.isInitialEventHighlighted,
|
||||
scroll_into_view: false,
|
||||
replyingToEvent: this.state.replyToEvent,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
|
@ -374,7 +375,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
editState={this.state.editState}
|
||||
eventId={this.props.initialEvent?.getId()}
|
||||
highlightedEventId={highlightedEventId}
|
||||
onUserScroll={this.resetHighlightedEvent}
|
||||
eventScrollIntoView={this.props.initialEventScrollIntoView}
|
||||
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
/>
|
||||
</div> }
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, ReactNode, SyntheticEvent } from 'react';
|
||||
import React, { createRef, ReactNode } from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
@ -40,8 +40,6 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import { Action } from '../../dispatcher/actions';
|
||||
import Timer from '../../utils/Timer';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import MessagePanel from "./MessagePanel";
|
||||
import { IScrollState } from "./ScrollPanel";
|
||||
|
@ -55,6 +53,7 @@ import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper";
|
|||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
|
@ -92,6 +91,9 @@ interface IProps {
|
|||
// id of an event to jump to. If not given, will go to the end of the live timeline.
|
||||
eventId?: string;
|
||||
|
||||
// whether we should scroll the event into view
|
||||
eventScrollIntoView?: boolean;
|
||||
|
||||
// where to position the event given by eventId, in pixels from the bottom of the viewport.
|
||||
// If not given, will try to put the event half way down the viewport.
|
||||
eventPixelOffset?: number;
|
||||
|
@ -125,8 +127,7 @@ interface IProps {
|
|||
// callback which is called when the panel is scrolled.
|
||||
onScroll?(event: Event): void;
|
||||
|
||||
// callback which is called when the user interacts with the room timeline
|
||||
onUserScroll?(event: SyntheticEvent): void;
|
||||
onEventScrolledIntoView?(eventId?: string): void;
|
||||
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated?(): void;
|
||||
|
@ -211,7 +212,6 @@ interface IEventIndexOpts {
|
|||
*
|
||||
* Also responsible for handling and sending read receipts.
|
||||
*/
|
||||
@replaceableComponent("structures.TimelinePanel")
|
||||
class TimelinePanel extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
|
@ -329,9 +329,11 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
|
||||
const differentEventId = newProps.eventId != this.props.eventId;
|
||||
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
|
||||
if (differentEventId || differentHighlightedEventId) {
|
||||
logger.log("TimelinePanel switching to eventId " + newProps.eventId +
|
||||
" (was " + this.props.eventId + ")");
|
||||
const differentAvoidJump = newProps.eventScrollIntoView && !this.props.eventScrollIntoView;
|
||||
if (differentEventId || differentHighlightedEventId || differentAvoidJump) {
|
||||
logger.log("TimelinePanel switching to " +
|
||||
"eventId " + newProps.eventId + " (was " + this.props.eventId + "), " +
|
||||
"scrollIntoView: " + newProps.eventScrollIntoView + " (was " + this.props.eventScrollIntoView + ")");
|
||||
return this.initTimeline(newProps);
|
||||
}
|
||||
}
|
||||
|
@ -1125,7 +1127,41 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
offsetBase = 0.5;
|
||||
}
|
||||
|
||||
return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
|
||||
return this.loadTimeline(initialEvent, pixelOffset, offsetBase, props.eventScrollIntoView);
|
||||
}
|
||||
|
||||
private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
|
||||
const doScroll = () => {
|
||||
if (eventId) {
|
||||
debuglog("TimelinePanel scrolling to eventId " + eventId +
|
||||
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
|
||||
this.messagePanel.current.scrollToEvent(
|
||||
eventId,
|
||||
pixelOffset,
|
||||
offsetBase,
|
||||
);
|
||||
} else {
|
||||
debuglog("TimelinePanel scrolling to bottom");
|
||||
this.messagePanel.current.scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
debuglog("TimelinePanel scheduling scroll to event");
|
||||
this.props.onEventScrolledIntoView?.(eventId);
|
||||
// Ensure the correct scroll position pre render, if the messages have already been loaded to DOM,
|
||||
// to avoid it jumping around
|
||||
doScroll();
|
||||
|
||||
// Ensure the correct scroll position post render for correct behaviour.
|
||||
//
|
||||
// requestAnimationFrame runs our code immediately after the DOM update but before the next repaint.
|
||||
//
|
||||
// If the messages have just been loaded for the first time, this ensures we'll repeat setting the
|
||||
// correct scroll position after React has re-rendered the TimelinePanel and MessagePanel and
|
||||
// updated the DOM.
|
||||
window.requestAnimationFrame(() => {
|
||||
doScroll();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1141,8 +1177,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
* @param {number?} offsetBase the reference point for the pixelOffset. 0
|
||||
* means the top of the container, 1 means the bottom, and fractional
|
||||
* values mean somewhere in the middle. If omitted, it defaults to 0.
|
||||
*
|
||||
* @param {boolean?} scrollIntoView whether to scroll the event into view.
|
||||
*/
|
||||
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
|
||||
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
|
||||
this.timelineWindow = new TimelineWindow(
|
||||
MatrixClientPeg.get(), this.props.timelineSet,
|
||||
{ windowLimit: this.props.timelineCap });
|
||||
|
@ -1177,11 +1215,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
"messagePanel didn't load");
|
||||
return;
|
||||
}
|
||||
if (eventId) {
|
||||
this.messagePanel.current.scrollToEvent(eventId, pixelOffset,
|
||||
offsetBase);
|
||||
} else {
|
||||
this.messagePanel.current.scrollToBottom();
|
||||
|
||||
if (scrollIntoView) {
|
||||
this.scrollIntoView(eventId, pixelOffset, offsetBase);
|
||||
}
|
||||
|
||||
if (this.props.sendReadReceiptOnLoad) {
|
||||
|
@ -1475,7 +1511,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
|
||||
const shouldIgnore = !!ev.status || // local echo
|
||||
(ignoreOwn && ev.getSender() === myUserId); // own message
|
||||
const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
|
||||
const isWithoutTile = !haveRendererForEvent(ev, this.context?.showHiddenEvents) ||
|
||||
shouldHideEvent(ev, this.context);
|
||||
|
||||
if (isWithoutTile || !node) {
|
||||
|
@ -1632,7 +1668,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||
stickyBottom={stickyBottom}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onUserScroll={this.props.onUserScroll}
|
||||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
|
||||
|
|
|
@ -18,14 +18,12 @@ import * as React from "react";
|
|||
import classNames from "classnames";
|
||||
|
||||
import ToastStore, { IToast } from "../../stores/ToastStore";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
interface IState {
|
||||
toasts: IToast<any>[];
|
||||
countSeen: number;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ToastContainer")
|
||||
export default class ToastContainer extends React.Component<{}, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -81,7 +79,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
|||
titleElement = (
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{ title }</h2>
|
||||
<span>{ countIndicator }</span>
|
||||
<span className="mx_Toast_title_countIndicator">{ countIndicator }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ import { Action } from "../../dispatcher/actions";
|
|||
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 {
|
||||
|
@ -40,7 +39,6 @@ interface IState {
|
|||
uploadsHere: IUpload[];
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.UploadBar")
|
||||
export default class UploadBar extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import { ActionPayload } from "../../dispatcher/payloads";
|
|||
import { Action } from "../../dispatcher/actions";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ChevronFace, ContextMenuButton } from "./ContextMenu";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import { UserTab } from "../views/dialogs/UserTab";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
import Modal from "../../Modal";
|
||||
|
@ -53,7 +53,6 @@ import { UIFeature } from "../../settings/UIFeature";
|
|||
import HostSignupAction from "./HostSignupAction";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
|
||||
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
|
||||
|
@ -143,7 +142,6 @@ const below = (rect: PartialDOMRect) => {
|
|||
};
|
||||
};
|
||||
|
||||
@replaceableComponent("structures.UserMenu")
|
||||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
|
|
|
@ -23,7 +23,6 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
import HomePage from "./HomePage";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import MainSplit from "./MainSplit";
|
||||
import RightPanel from "./RightPanel";
|
||||
|
@ -41,7 +40,6 @@ interface IState {
|
|||
member?: RoomMember;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.UserView")
|
||||
export default class UserView extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
|
@ -24,12 +24,12 @@ import { _t } from "../../languageHandler";
|
|||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { canEditContent } from "../../utils/EventUtils";
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool";
|
||||
import { StateEventEditor } from "../views/dialogs/devtools/RoomState";
|
||||
import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event";
|
||||
import CopyableText from "../views/elements/CopyableText";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
|
||||
|
@ -39,7 +39,6 @@ interface IState {
|
|||
isEditing: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ViewSource")
|
||||
export default class ViewSource extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -65,29 +64,58 @@ export default class ViewSource extends React.Component<IProps, IState> {
|
|||
// @ts-ignore
|
||||
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
||||
const originalEventSource = mxEvent.event;
|
||||
|
||||
const copyOriginalFunc = (): string => {
|
||||
return stringify(originalEventSource);
|
||||
};
|
||||
if (isEncrypted) {
|
||||
const copyDecryptedFunc = (): string => {
|
||||
return stringify(decryptedEventSource);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<details open className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{ _t("Decrypted event source") }</span>
|
||||
<span className="mx_ViewSource_heading">
|
||||
{ _t("Decrypted event source") }
|
||||
</span>
|
||||
</summary>
|
||||
<SyntaxHighlight language="json">{ stringify(decryptedEventSource) }</SyntaxHighlight>
|
||||
<div className="mx_ViewSource_container">
|
||||
<CopyableText getTextToCopy={copyDecryptedFunc}>
|
||||
<SyntaxHighlight language="json">
|
||||
{ stringify(decryptedEventSource) }
|
||||
</SyntaxHighlight>
|
||||
</CopyableText>
|
||||
</div>
|
||||
</details>
|
||||
<details className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{ _t("Original event source") }</span>
|
||||
<span className="mx_ViewSource_heading">
|
||||
{ _t("Original event source") }
|
||||
</span>
|
||||
</summary>
|
||||
<SyntaxHighlight language="json">{ stringify(originalEventSource) }</SyntaxHighlight>
|
||||
<div className="mx_ViewSource_container">
|
||||
<CopyableText getTextToCopy={copyOriginalFunc}>
|
||||
<SyntaxHighlight language="json">
|
||||
{ stringify(originalEventSource) }
|
||||
</SyntaxHighlight>
|
||||
</CopyableText>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="mx_ViewSource_heading">{ _t("Original event source") }</div>
|
||||
<SyntaxHighlight language="json">{ stringify(originalEventSource) }</SyntaxHighlight>
|
||||
<div className="mx_ViewSource_heading">
|
||||
{ _t("Original event source") }
|
||||
</div>
|
||||
<div className="mx_ViewSource_container">
|
||||
<CopyableText getTextToCopy={copyOriginalFunc}>
|
||||
<SyntaxHighlight language="json">
|
||||
{ stringify(originalEventSource) }
|
||||
</SyntaxHighlight>
|
||||
</CopyableText>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -139,8 +167,16 @@ export default class ViewSource extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
|
||||
<div>
|
||||
<div>{ _t("Room ID: %(roomId)s", { roomId }) }</div>
|
||||
<div>{ _t("Event ID: %(eventId)s", { eventId }) }</div>
|
||||
<div>
|
||||
<CopyableText getTextToCopy={() => roomId} border={false}>
|
||||
{ _t("Room ID: %(roomId)s", { roomId }) }
|
||||
</CopyableText>
|
||||
</div>
|
||||
<div>
|
||||
<CopyableText getTextToCopy={() => eventId} border={false}>
|
||||
{ _t("Event ID: %(eventId)s", { eventId }) }
|
||||
</CopyableText>
|
||||
</div>
|
||||
<div className="mx_ViewSource_separator" />
|
||||
{ isEditing ? this.editSourceContent() : this.viewSourceContent() }
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,6 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
|
@ -33,7 +32,6 @@ interface IState {
|
|||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.CompleteSecurity")
|
||||
export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
|
@ -19,7 +19,6 @@ import React from 'react';
|
|||
import AuthPage from '../../views/auth/AuthPage';
|
||||
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
|
||||
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
|
@ -27,7 +26,6 @@ interface IProps {
|
|||
tokenLogin?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.E2eSetup")
|
||||
export default class E2eSetup extends React.Component<IProps> {
|
||||
render() {
|
||||
return (
|
||||
|
|
|
@ -28,7 +28,6 @@ import AuthPage from "../../views/auth/AuthPage";
|
|||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
import EmailField from "../../views/auth/EmailField";
|
||||
import PassphraseField from '../../views/auth/PassphraseField';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
|
||||
import InlineSpinner from '../../views/elements/InlineSpinner';
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
|
@ -81,7 +80,6 @@ enum ForgotPasswordField {
|
|||
PasswordConfirm = 'field_password_confirm',
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.ForgotPassword")
|
||||
export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||
private reset: PasswordReset;
|
||||
|
||||
|
@ -224,8 +222,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onInputChanged = (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => {
|
||||
let value = ev.currentTarget.value;
|
||||
if (stateKey === "email") value = value.trim();
|
||||
this.setState({
|
||||
[stateKey]: ev.currentTarget.value,
|
||||
[stateKey]: value,
|
||||
} as any);
|
||||
};
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ import InlineSpinner from "../../views/elements/InlineSpinner";
|
|||
import Spinner from "../../views/elements/Spinner";
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AuthBody from "../../views/auth/AuthBody";
|
||||
import AuthHeader from "../../views/auth/AuthHeader";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
|
@ -103,7 +102,6 @@ interface IState {
|
|||
/*
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
*/
|
||||
@replaceableComponent("structures.auth.LoginComponent")
|
||||
export default class LoginComponent extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private loginLogic: Login;
|
||||
|
|
|
@ -30,7 +30,6 @@ import Login, { ISSOFlow } from "../../../Login";
|
|||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
import ServerPicker from '../../views/elements/ServerPicker';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RegistrationForm from '../../views/auth/RegistrationForm';
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
import AuthBody from "../../views/auth/AuthBody";
|
||||
|
@ -110,7 +109,6 @@ interface IState {
|
|||
ssoFlow?: ISSOFlow;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.Registration")
|
||||
export default class Registration extends React.Component<IProps, IState> {
|
||||
loginLogic: Login;
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import Modal from '../../../Modal';
|
||||
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
|
||||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
import Spinner from '../../views/elements/Spinner';
|
||||
|
@ -49,7 +48,6 @@ interface IState {
|
|||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.SetupEncryptionBody")
|
||||
export default class SetupEncryptionBody extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -27,7 +27,6 @@ import { ISSOFlow, LoginFlow, sendLoginRequest } from "../../../Login";
|
|||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ConfirmWipeDeviceDialog from '../../views/dialogs/ConfirmWipeDeviceDialog';
|
||||
import Field from '../../views/elements/Field';
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
|
@ -70,7 +69,6 @@ interface IState {
|
|||
flows: LoginFlow[];
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.SoftLogout")
|
||||
export default class SoftLogout extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
26
src/components/structures/static-page-vars.ts
Normal file
26
src/components/structures/static-page-vars.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
// We're importing via require specifically so the svg becomes a URI rather than a DOM element.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const matrixSvg = require('../../../res/img/matrix.svg').default;
|
||||
|
||||
/**
|
||||
* Intended to replace $matrixLogo in the welcome page.
|
||||
*/
|
||||
export const MATRIX_LOGO_HTML = `<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
|
||||
<img width="79" height="34" alt="Matrix" style="padding-left: 1px;vertical-align: middle" src="${matrixSvg}"/>
|
||||
</a>`;
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React, { ReactNode } from "react";
|
||||
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||
import DurationClock from "./DurationClock";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -25,7 +24,6 @@ import SeekBar from "./SeekBar";
|
|||
import PlaybackClock from "./PlaybackClock";
|
||||
import AudioPlayerBase from "./AudioPlayerBase";
|
||||
|
||||
@replaceableComponent("views.audio_messages.AudioPlayer")
|
||||
export default class AudioPlayer extends AudioPlayerBase {
|
||||
protected renderFileSize(): string {
|
||||
const bytes = this.props.playback.sizeBytes;
|
||||
|
|
|
@ -19,7 +19,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
@ -39,7 +38,6 @@ interface IState {
|
|||
error?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.audio_messages.AudioPlayerBase")
|
||||
export default abstract class AudioPlayerBase<T extends IProps = IProps> extends React.PureComponent<T, IState> {
|
||||
protected seekRef: RefObject<SeekBar> = createRef();
|
||||
protected playPauseRef: RefObject<PlayPauseButton> = createRef();
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React, { HTMLProps } from "react";
|
||||
|
||||
import { formatSeconds } from "../../../DateUtils";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
|
||||
seconds: number;
|
||||
|
@ -27,7 +26,6 @@ export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
|
|||
* Simply converts seconds into minutes and seconds. Note that hours will not be
|
||||
* displayed, making it possible to see "82:29".
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.Clock")
|
||||
export default class Clock extends React.Component<IProps> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Clock from "./Clock";
|
||||
import { Playback } from "../../../audio/Playback";
|
||||
|
||||
|
@ -31,7 +30,6 @@ interface IState {
|
|||
/**
|
||||
* A clock which shows a clip's maximum duration.
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.DurationClock")
|
||||
export default class DurationClock extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Clock from "./Clock";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
|
||||
|
@ -32,7 +31,6 @@ interface IState {
|
|||
/**
|
||||
* A clock for a live recording.
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.LiveRecordingClock")
|
||||
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
|
||||
private seconds = 0;
|
||||
private scheduledUpdate = new MarkedExecution(
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||
import Waveform from "./Waveform";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
|
@ -33,7 +32,6 @@ interface IState {
|
|||
/**
|
||||
* A waveform which shows the waveform of a live recording
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
|
||||
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
progress: 1,
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React, { ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||
|
@ -35,7 +34,6 @@ interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButto
|
|||
* Displays a play/pause button (activating the play/pause function of the recorder)
|
||||
* to be displayed in reference to a recording.
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.PlayPauseButton")
|
||||
export default class PlayPauseButton extends React.PureComponent<IProps> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Clock from "./Clock";
|
||||
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
@ -39,7 +38,6 @@ interface IState {
|
|||
/**
|
||||
* A clock for a playback of a recording.
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.PlaybackClock")
|
||||
export default class PlaybackClock extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
|
||||
import Waveform from "./Waveform";
|
||||
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback";
|
||||
|
@ -34,7 +33,6 @@ interface IState {
|
|||
/**
|
||||
* A waveform which shows the waveform of a previously recorded recording
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.PlaybackWaveform")
|
||||
export default class PlaybackWaveform extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -18,7 +18,6 @@ import React, { ReactNode } from "react";
|
|||
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import PlaybackClock from "./PlaybackClock";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
|
||||
import SeekBar from "./SeekBar";
|
||||
import PlaybackWaveform from "./PlaybackWaveform";
|
||||
|
@ -30,7 +29,6 @@ interface IProps extends IAudioPlayerBaseProps {
|
|||
withWaveform?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.audio_messages.RecordingPlayback")
|
||||
export default class RecordingPlayback extends AudioPlayerBase<IProps> {
|
||||
// This component is rendered in two ways: the composer and timeline. They have different
|
||||
// rendering properties (specifically the difference of a waveform or not).
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
|
||||
|
||||
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
import { percentageOf } from "../../../utils/numbers";
|
||||
|
||||
|
@ -43,7 +42,6 @@ interface ISeekCSS extends CSSProperties {
|
|||
|
||||
const ARROW_SKIP_SECONDS = 5; // arbitrary
|
||||
|
||||
@replaceableComponent("views.audio_messages.SeekBar")
|
||||
export default class SeekBar extends React.PureComponent<IProps, IState> {
|
||||
// We use an animation frame request to avoid overly spamming prop updates, even if we aren't
|
||||
// really using anything demanding on the CSS front.
|
||||
|
|
|
@ -17,8 +17,6 @@ limitations under the License.
|
|||
import React, { CSSProperties } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface WaveformCSSProperties extends CSSProperties {
|
||||
'--barHeight': number;
|
||||
}
|
||||
|
@ -39,7 +37,6 @@ interface IState {
|
|||
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
|
||||
* "filled", as a demonstration of the progress property.
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.Waveform")
|
||||
export default class Waveform extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
progress: 1,
|
||||
|
|
|
@ -16,9 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.auth.AuthBody")
|
||||
export default class AuthBody extends React.PureComponent {
|
||||
public render(): React.ReactNode {
|
||||
return <div className="mx_AuthBody">
|
||||
|
|
|
@ -19,9 +19,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.auth.AuthFooter")
|
||||
export default class AuthFooter extends React.Component {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AuthHeaderLogo from "./AuthHeaderLogo";
|
||||
import LanguageSelector from "./LanguageSelector";
|
||||
|
||||
|
@ -25,7 +24,6 @@ interface IProps {
|
|||
disableLanguageSelector?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.AuthHeader")
|
||||
export default class AuthHeader extends React.Component<IProps> {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
|
|
|
@ -16,9 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.auth.AuthHeaderLogo")
|
||||
export default class AuthHeaderLogo extends React.PureComponent {
|
||||
public render(): React.ReactNode {
|
||||
return <div className="mx_AuthHeaderLogo">
|
||||
|
|
|
@ -18,10 +18,8 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AuthFooter from "./AuthFooter";
|
||||
|
||||
@replaceableComponent("views.auth.AuthPage")
|
||||
export default class AuthPage extends React.PureComponent {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
|
|
|
@ -18,7 +18,6 @@ import React, { createRef } from 'react';
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
||||
|
@ -35,7 +34,6 @@ interface ICaptchaFormState {
|
|||
/**
|
||||
* A pure UI component which displays a captcha form.
|
||||
*/
|
||||
@replaceableComponent("views.auth.CaptchaForm")
|
||||
export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICaptchaFormState> {
|
||||
static defaultProps = {
|
||||
onCaptchaResponse: () => {},
|
||||
|
|
|
@ -16,9 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.auth.CompleteSecurityBody")
|
||||
export default class CompleteSecurityBody extends React.PureComponent {
|
||||
public render(): React.ReactNode {
|
||||
return <div className="mx_CompleteSecurityBody">
|
||||
|
|
|
@ -19,7 +19,6 @@ import React from 'react';
|
|||
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Dropdown from "../elements/Dropdown";
|
||||
|
||||
const COUNTRIES_BY_ISO2 = {};
|
||||
|
@ -53,7 +52,6 @@ interface IState {
|
|||
defaultCountry: PhoneNumberCountryDefinition;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.CountryDropdown")
|
||||
export default class CountryDropdown extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React, { PureComponent, RefCallback, RefObject } from "react";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field, { IInputProps } from "../elements/Field";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
|
||||
|
@ -39,7 +38,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
|
|||
onValidate?(result: IValidationResult): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailField")
|
||||
class EmailField extends PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
label: _td("Email"),
|
||||
|
|
|
@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { LocalisedPolicy, Policies } from '../../../Terms';
|
||||
import Field from '../elements/Field';
|
||||
import CaptchaForm from "./CaptchaForm";
|
||||
|
@ -93,7 +92,6 @@ interface IPasswordAuthEntryState {
|
|||
password: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.PasswordAuthEntry")
|
||||
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Password;
|
||||
|
||||
|
@ -191,7 +189,6 @@ interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
|
|||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
||||
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
|
||||
static LOGIN_TYPE = AuthType.Recaptcha;
|
||||
|
||||
|
@ -262,7 +259,6 @@ interface ITermsAuthEntryState {
|
|||
errorText?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.TermsAuthEntry")
|
||||
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Terms;
|
||||
|
||||
|
@ -409,7 +405,6 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
|
|||
};
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
||||
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
|
||||
static LOGIN_TYPE = AuthType.Email;
|
||||
|
||||
|
@ -472,7 +467,6 @@ interface IMsisdnAuthEntryState {
|
|||
errorText: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.MsisdnAuthEntry")
|
||||
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Msisdn;
|
||||
|
||||
|
@ -623,7 +617,6 @@ interface ISSOAuthEntryState {
|
|||
attemptFailed: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.SSOAuthEntry")
|
||||
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Sso;
|
||||
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
|
||||
|
@ -743,7 +736,6 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.FallbackAuthEntry")
|
||||
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||
private popupWindow: Window;
|
||||
private fallbackButton = createRef<HTMLButtonElement>();
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React, { PureComponent, RefCallback, RefObject } from "react";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field, { IInputProps } from "../elements/Field";
|
||||
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
|
@ -35,7 +34,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
|
|||
onValidate?(result: IValidationResult);
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailField")
|
||||
class PassphraseConfirmField extends PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
label: _td("Confirm password"),
|
||||
|
|
|
@ -22,7 +22,6 @@ import SdkConfig from "../../../SdkConfig";
|
|||
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import Field, { IInputProps } from "../elements/Field";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps extends Omit<IInputProps, "onValidate"> {
|
||||
autoFocus?: boolean;
|
||||
|
@ -41,7 +40,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
|
|||
onValidate?(result: IValidationResult);
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.PassphraseField")
|
||||
class PassphraseField extends PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
label: _td("Password"),
|
||||
|
|
|
@ -24,7 +24,6 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import withValidation, { IValidationResult } from "../elements/Validation";
|
||||
import Field from "../elements/Field";
|
||||
import CountryDropdown from "./CountryDropdown";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import EmailField from "./EmailField";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
|
@ -66,7 +65,6 @@ enum LoginField {
|
|||
* A pure UI component which displays a username/password form.
|
||||
* The email/username/phone fields are fully-controlled, the password field is not.
|
||||
*/
|
||||
@replaceableComponent("views.auth.PasswordLogin")
|
||||
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onUsernameChanged: function() {},
|
||||
|
|
|
@ -31,7 +31,6 @@ import EmailField from "./EmailField";
|
|||
import PassphraseField from "./PassphraseField";
|
||||
import Field from '../elements/Field';
|
||||
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import CountryDropdown from "./CountryDropdown";
|
||||
import PassphraseConfirmField from "./PassphraseConfirmField";
|
||||
|
||||
|
@ -92,7 +91,6 @@ interface IState {
|
|||
/*
|
||||
* A pure UI component which displays a registration form.
|
||||
*/
|
||||
@replaceableComponent("views.auth.RegistrationForm")
|
||||
export default class RegistrationForm extends React.PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onValidationChange: logger.error,
|
||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import classNames from "classnames";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import AuthPage from "./AuthPage";
|
||||
import { _td } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import LanguageSelector from "./LanguageSelector";
|
||||
import EmbeddedPage from "../../structures/EmbeddedPage";
|
||||
import { MATRIX_LOGO_HTML } from "../../structures/static-page-vars";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
@ -33,12 +33,8 @@ interface IProps {
|
|||
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.Welcome")
|
||||
export default class Welcome extends React.PureComponent<IProps> {
|
||||
public render(): React.ReactNode {
|
||||
// FIXME: Using an import will result in wrench-element-tests failures
|
||||
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
|
||||
|
||||
const pagesConfig = SdkConfig.getObject("embedded_pages");
|
||||
let pageUrl = null;
|
||||
if (pagesConfig) {
|
||||
|
@ -59,6 +55,8 @@ export default class Welcome extends React.PureComponent<IProps> {
|
|||
replaceMap={{
|
||||
"$riot:ssoUrl": "#/start_sso",
|
||||
"$riot:casUrl": "#/start_cas",
|
||||
"$matrixLogo": MATRIX_LOGO_HTML,
|
||||
"[matrix]": MATRIX_LOGO_HTML,
|
||||
}}
|
||||
/>
|
||||
<LanguageSelector />
|
||||
|
|
|
@ -32,7 +32,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import TooltipTarget from "../elements/TooltipTarget";
|
||||
|
||||
|
@ -78,7 +77,6 @@ function tooltipText(variant: Icon) {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.avatars.DecoratedRoomAvatar")
|
||||
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
|
||||
private _dmUser: User;
|
||||
private isUnmounted = false;
|
||||
|
|
|
@ -23,9 +23,8 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { CardContext } from '../right_panel/BaseCard';
|
||||
import { CardContext } from '../right_panel/context';
|
||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
@ -52,7 +51,6 @@ interface IState {
|
|||
imageUrl?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.avatars.MemberAvatar")
|
||||
export default class MemberAvatar extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
width: 40,
|
||||
|
|
|
@ -28,7 +28,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import Modal from '../../../Modal';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||
|
||||
|
@ -52,7 +51,6 @@ interface IState {
|
|||
urls: string[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.avatars.RoomAvatar")
|
||||
export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
width: 36,
|
||||
|
|
65
src/components/views/beacon/BeaconMarker.tsx
Normal file
65
src/components/views/beacon/BeaconMarker.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2022 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, { useContext } from 'react';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import {
|
||||
Beacon,
|
||||
BeaconEvent,
|
||||
} from 'matrix-js-sdk/src/matrix';
|
||||
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
||||
|
||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||
import SmartMarker from '../location/SmartMarker';
|
||||
|
||||
interface Props {
|
||||
map: maplibregl.Map;
|
||||
beacon: Beacon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a map SmartMarker with latest location from given beacon
|
||||
*/
|
||||
const BeaconMarker: React.FC<Props> = ({ map, beacon }) => {
|
||||
const latestLocationState = useEventEmitterState(
|
||||
beacon,
|
||||
BeaconEvent.LocationUpdate,
|
||||
() => beacon.latestLocationState,
|
||||
);
|
||||
const matrixClient = useContext(MatrixClientContext);
|
||||
const room = matrixClient.getRoom(beacon.roomId);
|
||||
|
||||
if (!latestLocationState || !beacon.isLive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const geoUri = latestLocationState?.uri;
|
||||
|
||||
const markerRoomMember = beacon.beaconInfo.assetType === LocationAssetType.Self ?
|
||||
room.getMember(beacon.beaconInfoOwner) :
|
||||
undefined;
|
||||
|
||||
return <SmartMarker
|
||||
map={map}
|
||||
id={beacon.identifier}
|
||||
geoUri={geoUri}
|
||||
roomMember={markerRoomMember}
|
||||
useMemberColor
|
||||
/>;
|
||||
};
|
||||
|
||||
export default BeaconMarker;
|
84
src/components/views/beacon/BeaconStatus.tsx
Normal file
84
src/components/views/beacon/BeaconStatus.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2022 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, { HTMLProps } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Beacon } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import LiveTimeRemaining from './LiveTimeRemaining';
|
||||
import { BeaconDisplayStatus } from './displayStatus';
|
||||
import { getBeaconExpiryTimestamp } from '../../../utils/beacon';
|
||||
import { formatTime } from '../../../DateUtils';
|
||||
|
||||
interface Props {
|
||||
displayStatus: BeaconDisplayStatus;
|
||||
displayLiveTimeRemaining?: boolean;
|
||||
beacon?: Beacon;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const BeaconExpiryTime: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
|
||||
const expiryTime = formatTime(new Date(getBeaconExpiryTimestamp(beacon)));
|
||||
return <span className='mx_BeaconStatus_expiryTime'>{ _t('Live until %(expiryTime)s', { expiryTime }) }</span>;
|
||||
};
|
||||
|
||||
const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> =
|
||||
({
|
||||
beacon,
|
||||
displayStatus,
|
||||
displayLiveTimeRemaining,
|
||||
label,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}) => {
|
||||
const isIdle = displayStatus === BeaconDisplayStatus.Loading ||
|
||||
displayStatus === BeaconDisplayStatus.Stopped;
|
||||
|
||||
return <div
|
||||
{...rest}
|
||||
className={classNames('mx_BeaconStatus', `mx_BeaconStatus_${displayStatus}`, className)}
|
||||
>
|
||||
<StyledLiveBeaconIcon
|
||||
className='mx_BeaconStatus_icon'
|
||||
withError={displayStatus === BeaconDisplayStatus.Error}
|
||||
isIdle={isIdle}
|
||||
/>
|
||||
<div className='mx_BeaconStatus_description'>
|
||||
|
||||
{ displayStatus === BeaconDisplayStatus.Loading && <span>{ _t('Loading live location...') }</span> }
|
||||
{ displayStatus === BeaconDisplayStatus.Stopped && <span>{ _t('Live location ended') }</span> }
|
||||
|
||||
{ displayStatus === BeaconDisplayStatus.Error && <span>{ _t('Live location error') }</span> }
|
||||
|
||||
{ displayStatus === BeaconDisplayStatus.Active && beacon && <>
|
||||
<>
|
||||
{ label }
|
||||
{ displayLiveTimeRemaining ?
|
||||
<LiveTimeRemaining beacon={beacon} /> :
|
||||
<BeaconExpiryTime beacon={beacon} />
|
||||
}
|
||||
</>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
{ children }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default BeaconStatus;
|
116
src/components/views/beacon/BeaconViewDialog.tsx
Normal file
116
src/components/views/beacon/BeaconViewDialog.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2022 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 { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import {
|
||||
Beacon,
|
||||
Room,
|
||||
} from 'matrix-js-sdk/src/matrix';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
|
||||
import { useLiveBeacons } from '../../../utils/beacon/useLiveBeacons';
|
||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||
import BaseDialog from "../dialogs/BaseDialog";
|
||||
import { IDialogProps } from "../dialogs/IDialogProps";
|
||||
import Map from '../location/Map';
|
||||
import ZoomButtons from '../location/ZoomButtons';
|
||||
import BeaconMarker from './BeaconMarker';
|
||||
import { Bounds, getBeaconBounds } from '../../../utils/beacon/bounds';
|
||||
import { getGeoUri } from '../../../utils/beacon';
|
||||
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
roomId: Room['roomId'];
|
||||
matrixClient: MatrixClient;
|
||||
// open the map centered on this beacon's location
|
||||
focusBeacon?: Beacon;
|
||||
}
|
||||
|
||||
const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
||||
if (!bounds) {
|
||||
return;
|
||||
}
|
||||
return getGeoUri({
|
||||
latitude: (bounds.north + bounds.south) / 2,
|
||||
longitude: (bounds.east + bounds.west) / 2,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dialog to view live beacons maximised
|
||||
*/
|
||||
const BeaconViewDialog: React.FC<IProps> = ({
|
||||
focusBeacon,
|
||||
roomId,
|
||||
matrixClient,
|
||||
onFinished,
|
||||
}) => {
|
||||
const liveBeacons = useLiveBeacons(roomId, matrixClient);
|
||||
|
||||
const bounds = getBeaconBounds(liveBeacons);
|
||||
const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_BeaconViewDialog'
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
{ !!bounds ? <Map
|
||||
id='mx_BeaconViewDialog'
|
||||
bounds={bounds}
|
||||
centerGeoUri={centerGeoUri}
|
||||
interactive
|
||||
className="mx_BeaconViewDialog_map"
|
||||
>
|
||||
{
|
||||
({ map }: { map: maplibregl.Map}) =>
|
||||
<>
|
||||
{ liveBeacons.map(beacon => <BeaconMarker
|
||||
key={beacon.identifier}
|
||||
map={map}
|
||||
beacon={beacon}
|
||||
/>) }
|
||||
<ZoomButtons map={map} />
|
||||
</>
|
||||
}
|
||||
</Map> :
|
||||
<div
|
||||
data-test-id='beacon-view-dialog-map-fallback'
|
||||
className='mx_BeaconViewDialog_map mx_BeaconViewDialog_mapFallback'
|
||||
>
|
||||
<LocationIcon className='mx_BeaconViewDialog_mapFallbackIcon' />
|
||||
<span className='mx_BeaconViewDialog_mapFallbackMessage'>{ _t('No live locations') }</span>
|
||||
<AccessibleButton
|
||||
kind='primary'
|
||||
onClick={onFinished}
|
||||
data-test-id='beacon-view-dialog-fallback-close'
|
||||
>
|
||||
{ _t('Close') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
}
|
||||
</MatrixClientContext.Provider>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BeaconViewDialog;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue