Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/15051
This commit is contained in:
commit
0e2f617d94
217 changed files with 4927 additions and 2789 deletions
50
src/@types/diff-dom.ts
Normal file
50
src/@types/diff-dom.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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 "diff-dom" {
|
||||
enum Action {
|
||||
AddElement = "addElement",
|
||||
AddTextElement = "addTextElement",
|
||||
RemoveTextElement = "removeTextElement",
|
||||
RemoveElement = "removeElement",
|
||||
ReplaceElement = "replaceElement",
|
||||
ModifyTextElement = "modifyTextElement",
|
||||
AddAttribute = "addAttribute",
|
||||
RemoveAttribute = "removeAttribute",
|
||||
ModifyAttribute = "modifyAttribute",
|
||||
}
|
||||
|
||||
export interface IDiff {
|
||||
action: Action;
|
||||
name: string;
|
||||
text?: string;
|
||||
route: number[];
|
||||
value: string;
|
||||
element: unknown;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
}
|
||||
|
||||
export class DiffDOM {
|
||||
public constructor(opts?: IOpts);
|
||||
public apply(tree: unknown, diffs: IDiff[]): unknown;
|
||||
public undo(tree: unknown, diffs: IDiff[]): unknown;
|
||||
public diff(a: HTMLElement | string, b: HTMLElement | string): IDiff[];
|
||||
}
|
||||
}
|
15
src/@types/global.d.ts
vendored
15
src/@types/global.d.ts
vendored
|
@ -44,6 +44,7 @@ import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
|||
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
||||
import PerformanceMonitor from "../performance";
|
||||
import UIStore from "../stores/UIStore";
|
||||
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -84,6 +85,7 @@ declare global {
|
|||
mxPerformanceMonitor: PerformanceMonitor;
|
||||
mxPerformanceEntryNames: any;
|
||||
mxUIStore: UIStore;
|
||||
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
|
@ -111,19 +113,6 @@ declare global {
|
|||
usageDetails?: {[key: string]: number};
|
||||
}
|
||||
|
||||
export interface ISettledFulfilled<T> {
|
||||
status: "fulfilled";
|
||||
value: T;
|
||||
}
|
||||
export interface ISettledRejected {
|
||||
status: "rejected";
|
||||
reason: any;
|
||||
}
|
||||
|
||||
interface PromiseConstructor {
|
||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
type?: string;
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
|
|
|
@ -14,18 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import {User} from "matrix-js-sdk/src/models/user";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import {mediaFromMxc} from "./customisations/Media";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
||||
export function avatarUrlForMember(
|
||||
member: RoomMember,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeMethod: ResizeMethod,
|
||||
): string {
|
||||
let url: string;
|
||||
if (member?.getMxcAvatarUrl()) {
|
||||
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
|
@ -39,7 +43,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
|
|||
return url;
|
||||
}
|
||||
|
||||
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
export function avatarUrlForUser(
|
||||
user: Pick<User, "avatarUrl">,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeMethod?: ResizeMethod,
|
||||
): string | null {
|
||||
if (!user.avatarUrl) return null;
|
||||
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
|||
// (and store the ID of their native room)
|
||||
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
||||
|
||||
enum AudioID {
|
||||
export enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
Ringback = 'ringbackAudio',
|
||||
CallEnd = 'callendAudio',
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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 SettingsStore from "./settings/SettingsStore";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export default {
|
||||
hasAnyLabeledDevices: async function() {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.some(d => !!d.label);
|
||||
},
|
||||
|
||||
getDevices: function() {
|
||||
// Only needed for Electron atm, though should work in modern browsers
|
||||
// once permission has been granted to the webapp
|
||||
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
||||
const audiooutput = [];
|
||||
const audioinput = [];
|
||||
const videoinput = [];
|
||||
|
||||
devices.forEach((device) => {
|
||||
switch (device.kind) {
|
||||
case 'audiooutput': audiooutput.push(device); break;
|
||||
case 'audioinput': audioinput.push(device); break;
|
||||
case 'videoinput': videoinput.push(device); break;
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("Loaded WebRTC Devices", mediaDevices);
|
||||
return {
|
||||
audiooutput,
|
||||
audioinput,
|
||||
videoinput,
|
||||
};
|
||||
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
|
||||
},
|
||||
|
||||
loadDevices: function() {
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
setMatrixCallAudioInput(audioDeviceId);
|
||||
setMatrixCallVideoInput(videoDeviceId);
|
||||
},
|
||||
|
||||
setAudioOutput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||
},
|
||||
|
||||
setAudioInput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallAudioInput(deviceId);
|
||||
},
|
||||
|
||||
setVideoInput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallVideoInput(deviceId);
|
||||
},
|
||||
|
||||
getAudioOutput: function() {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
||||
},
|
||||
|
||||
getAudioInput: function() {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
||||
},
|
||||
|
||||
getVideoInput: function() {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
||||
},
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 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.
|
||||
|
@ -14,34 +14,40 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
export class DecryptionFailure {
|
||||
constructor(failedEventId, errorCode) {
|
||||
this.failedEventId = failedEventId;
|
||||
this.errorCode = errorCode;
|
||||
public readonly ts: number;
|
||||
|
||||
constructor(public readonly failedEventId: string, public readonly errorCode: string) {
|
||||
this.ts = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
type TrackingFn = (count: number, trackedErrCode: string) => void;
|
||||
type ErrCodeMapFn = (errcode: string) => string;
|
||||
|
||||
export class DecryptionFailureTracker {
|
||||
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
|
||||
// is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
|
||||
// are accumulated in `failureCounts`.
|
||||
failures = [];
|
||||
public failures: DecryptionFailure[] = [];
|
||||
|
||||
// A histogram of the number of failures that will be tracked at the next tracking
|
||||
// interval, split by failure error code.
|
||||
failureCounts = {
|
||||
public failureCounts: Record<string, number> = {
|
||||
// [errorCode]: 42
|
||||
};
|
||||
|
||||
// Event IDs of failures that were tracked previously
|
||||
trackedEventHashMap = {
|
||||
public trackedEventHashMap: Record<string, boolean> = {
|
||||
// [eventId]: true
|
||||
};
|
||||
|
||||
// Set to an interval ID when `start` is called
|
||||
checkInterval = null;
|
||||
trackInterval = null;
|
||||
public checkInterval: NodeJS.Timeout = null;
|
||||
public trackInterval: NodeJS.Timeout = null;
|
||||
|
||||
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
|
||||
static TRACK_INTERVAL_MS = 60000;
|
||||
|
@ -67,7 +73,7 @@ export class DecryptionFailureTracker {
|
|||
* @param {function?} errorCodeMapFn The function used to map error codes to the
|
||||
* trackedErrorCode. If not provided, the `.code` of errors will be used.
|
||||
*/
|
||||
constructor(fn, errorCodeMapFn) {
|
||||
constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
|
||||
if (!fn || typeof fn !== 'function') {
|
||||
throw new Error('DecryptionFailureTracker requires tracking function');
|
||||
}
|
||||
|
@ -75,9 +81,6 @@ export class DecryptionFailureTracker {
|
|||
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
|
||||
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
|
||||
}
|
||||
|
||||
this._trackDecryptionFailure = fn;
|
||||
this._mapErrorCode = errorCodeMapFn;
|
||||
}
|
||||
|
||||
// loadTrackedEventHashMap() {
|
||||
|
@ -88,7 +91,7 @@ export class DecryptionFailureTracker {
|
|||
// localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
|
||||
// }
|
||||
|
||||
eventDecrypted(e, err) {
|
||||
public eventDecrypted(e: MatrixEvent, err: MatrixError | Error): void {
|
||||
if (err) {
|
||||
this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
|
||||
} else {
|
||||
|
@ -97,18 +100,18 @@ export class DecryptionFailureTracker {
|
|||
}
|
||||
}
|
||||
|
||||
addDecryptionFailure(failure) {
|
||||
public addDecryptionFailure(failure: DecryptionFailure): void {
|
||||
this.failures.push(failure);
|
||||
}
|
||||
|
||||
removeDecryptionFailuresForEvent(e) {
|
||||
public removeDecryptionFailuresForEvent(e: MatrixEvent): void {
|
||||
this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Start checking for and tracking failures.
|
||||
*/
|
||||
start() {
|
||||
public start(): void {
|
||||
this.checkInterval = setInterval(
|
||||
() => this.checkFailures(Date.now()),
|
||||
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
||||
|
@ -123,7 +126,7 @@ export class DecryptionFailureTracker {
|
|||
/**
|
||||
* Clear state and stop checking for and tracking failures.
|
||||
*/
|
||||
stop() {
|
||||
public stop(): void {
|
||||
clearInterval(this.checkInterval);
|
||||
clearInterval(this.trackInterval);
|
||||
|
||||
|
@ -132,11 +135,11 @@ export class DecryptionFailureTracker {
|
|||
}
|
||||
|
||||
/**
|
||||
* Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
|
||||
* Mark failures that occurred before nowTs - GRACE_PERIOD_MS as failures that should be
|
||||
* tracked. Only mark one failure per event ID.
|
||||
* @param {number} nowTs the timestamp that represents the time now.
|
||||
*/
|
||||
checkFailures(nowTs) {
|
||||
public checkFailures(nowTs: number): void {
|
||||
const failuresGivenGrace = [];
|
||||
const failuresNotReady = [];
|
||||
while (this.failures.length > 0) {
|
||||
|
@ -175,10 +178,10 @@ export class DecryptionFailureTracker {
|
|||
|
||||
const dedupedFailures = dedupedFailuresMap.values();
|
||||
|
||||
this._aggregateFailures(dedupedFailures);
|
||||
this.aggregateFailures(dedupedFailures);
|
||||
}
|
||||
|
||||
_aggregateFailures(failures) {
|
||||
private aggregateFailures(failures: DecryptionFailure[]): void {
|
||||
for (const failure of failures) {
|
||||
const errorCode = failure.errorCode;
|
||||
this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
|
||||
|
@ -189,12 +192,12 @@ export class DecryptionFailureTracker {
|
|||
* If there are failures that should be tracked, call the given trackDecryptionFailure
|
||||
* function with the number of failures that should be tracked.
|
||||
*/
|
||||
trackFailures() {
|
||||
public trackFailures(): void {
|
||||
for (const errorCode of Object.keys(this.failureCounts)) {
|
||||
if (this.failureCounts[errorCode] > 0) {
|
||||
const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode;
|
||||
const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode;
|
||||
|
||||
this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode);
|
||||
this.fn(this.failureCounts[errorCode], trackedErrorCode);
|
||||
this.failureCounts[errorCode] = 0;
|
||||
}
|
||||
}
|
|
@ -17,11 +17,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
||||
import cheerio from 'cheerio';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyMatrix from './linkify-matrix';
|
||||
import _linkifyElement from 'linkifyjs/element';
|
||||
import _linkifyString from 'linkifyjs/string';
|
||||
import classNames from 'classnames';
|
||||
|
@ -29,13 +28,15 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
|
|||
import url from 'url';
|
||||
import katex from 'katex';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import cheerio from 'cheerio';
|
||||
import { IContent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
||||
import linkifyMatrix from './linkify-matrix';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||
import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
|
||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||
import {mediaFromMxc} from "./customisations/Media";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -66,7 +67,7 @@ export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'
|
|||
* need emojification.
|
||||
* unicodeToImage uses this function.
|
||||
*/
|
||||
function mightContainEmoji(str: string) {
|
||||
function mightContainEmoji(str: string): boolean {
|
||||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||
}
|
||||
|
||||
|
@ -76,7 +77,7 @@ function mightContainEmoji(str: string) {
|
|||
* @param {String} char The emoji character
|
||||
* @return {String} The shortcode (such as :thumbup:)
|
||||
*/
|
||||
export function unicodeToShortcode(char: string) {
|
||||
export function unicodeToShortcode(char: string): string {
|
||||
const data = getEmojiFromUnicode(char);
|
||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||
}
|
||||
|
@ -87,7 +88,7 @@ export function unicodeToShortcode(char: string) {
|
|||
* @param {String} shortcode The shortcode (such as :thumbup:)
|
||||
* @return {String} The emoji character; null if none exists
|
||||
*/
|
||||
export function shortcodeToUnicode(shortcode: string) {
|
||||
export function shortcodeToUnicode(shortcode: string): string {
|
||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
||||
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
||||
return data ? data.unicode : null;
|
||||
|
@ -124,13 +125,13 @@ export function processHtmlForSending(html: string): string {
|
|||
* Given an untrusted HTML string, return a React node with an sanitized version
|
||||
* of that HTML.
|
||||
*/
|
||||
export function sanitizedHtmlNode(insaneHtml: string) {
|
||||
export function sanitizedHtmlNode(insaneHtml: string): ReactNode {
|
||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||
}
|
||||
|
||||
export function getHtmlText(insaneHtml: string) {
|
||||
export function getHtmlText(insaneHtml: string): string {
|
||||
return sanitizeHtml(insaneHtml, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {},
|
||||
|
@ -148,7 +149,7 @@ export function getHtmlText(insaneHtml: string) {
|
|||
* other places we need to sanitise URLs.
|
||||
* @return true if permitted, otherwise false
|
||||
*/
|
||||
export function isUrlPermitted(inputUrl: string) {
|
||||
export function isUrlPermitted(inputUrl: string): boolean {
|
||||
try {
|
||||
const parsed = url.parse(inputUrl);
|
||||
if (!parsed.protocol) return false;
|
||||
|
@ -351,13 +352,6 @@ class HtmlHighlighter extends BaseHighlighter<string> {
|
|||
}
|
||||
}
|
||||
|
||||
interface IContent {
|
||||
format?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
formatted_body?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
highlightLink?: string;
|
||||
disableBigEmoji?: boolean;
|
||||
|
@ -367,6 +361,14 @@ interface IOpts {
|
|||
ref?: React.Ref<any>;
|
||||
}
|
||||
|
||||
export interface IOptsReturnNode extends IOpts {
|
||||
returnString: false;
|
||||
}
|
||||
|
||||
export interface IOptsReturnString extends IOpts {
|
||||
returnString: true;
|
||||
}
|
||||
|
||||
/* turn a matrix event body into html
|
||||
*
|
||||
* content: 'content' of the MatrixEvent
|
||||
|
@ -380,6 +382,8 @@ interface IOpts {
|
|||
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
||||
* opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString)
|
||||
*/
|
||||
export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnString): string;
|
||||
export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnNode): ReactNode;
|
||||
export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) {
|
||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||
let bodyHasEmoji = false;
|
||||
|
@ -501,7 +505,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
|||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||
* @returns {string} Linkified string
|
||||
*/
|
||||
export function linkifyString(str: string, options = linkifyMatrix.options) {
|
||||
export function linkifyString(str: string, options = linkifyMatrix.options): string {
|
||||
return _linkifyString(str, options);
|
||||
}
|
||||
|
||||
|
@ -512,7 +516,7 @@ export function linkifyString(str: string, options = linkifyMatrix.options) {
|
|||
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
|
||||
* @returns {object}
|
||||
*/
|
||||
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) {
|
||||
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement {
|
||||
return _linkifyElement(element, options);
|
||||
}
|
||||
|
||||
|
@ -523,7 +527,7 @@ export function linkifyElement(element: HTMLElement, options = linkifyMatrix.opt
|
|||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||
* @returns {string}
|
||||
*/
|
||||
export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options) {
|
||||
export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options): string {
|
||||
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
|
||||
}
|
||||
|
||||
|
@ -534,7 +538,7 @@ export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatri
|
|||
* @param {Node} node
|
||||
* @returns {bool}
|
||||
*/
|
||||
export function checkBlockNode(node: Node) {
|
||||
export function checkBlockNode(node: Node): boolean {
|
||||
switch (node.nodeName) {
|
||||
case "H1":
|
||||
case "H2":
|
||||
|
|
120
src/MediaDeviceHandler.ts
Normal file
120
src/MediaDeviceHandler.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 SettingsStore from "./settings/SettingsStore";
|
||||
import { SettingLevel } from "./settings/SettingLevel";
|
||||
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
||||
import EventEmitter from 'events';
|
||||
|
||||
interface IMediaDevices {
|
||||
audioOutput: Array<MediaDeviceInfo>;
|
||||
audioInput: Array<MediaDeviceInfo>;
|
||||
videoInput: Array<MediaDeviceInfo>;
|
||||
}
|
||||
|
||||
export enum MediaDeviceHandlerEvent {
|
||||
AudioOutputChanged = "audio_output_changed",
|
||||
}
|
||||
|
||||
export default class MediaDeviceHandler extends EventEmitter {
|
||||
private static internalInstance;
|
||||
|
||||
public static get instance(): MediaDeviceHandler {
|
||||
if (!MediaDeviceHandler.internalInstance) {
|
||||
MediaDeviceHandler.internalInstance = new MediaDeviceHandler();
|
||||
}
|
||||
return MediaDeviceHandler.internalInstance;
|
||||
}
|
||||
|
||||
public static async hasAnyLabeledDevices(): Promise<boolean> {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.some(d => Boolean(d.label));
|
||||
}
|
||||
|
||||
public static async getDevices(): Promise<IMediaDevices> {
|
||||
// Only needed for Electron atm, though should work in modern browsers
|
||||
// once permission has been granted to the webapp
|
||||
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
|
||||
const audioOutput = [];
|
||||
const audioInput = [];
|
||||
const videoInput = [];
|
||||
|
||||
devices.forEach((device) => {
|
||||
switch (device.kind) {
|
||||
case 'audiooutput': audioOutput.push(device); break;
|
||||
case 'audioinput': audioInput.push(device); break;
|
||||
case 'videoinput': videoInput.push(device); break;
|
||||
}
|
||||
});
|
||||
|
||||
return { audioOutput, audioInput, videoInput };
|
||||
} catch (error) {
|
||||
console.warn('Unable to refresh WebRTC Devices: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
|
||||
*/
|
||||
public static loadDevices(): void {
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
setMatrixCallAudioInput(audioDeviceId);
|
||||
setMatrixCallVideoInput(videoDeviceId);
|
||||
}
|
||||
|
||||
public setAudioOutput(deviceId: string): void {
|
||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||
this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will not change the device that a potential call uses. The call will
|
||||
* need to be ended and started again for this change to take effect
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
public setAudioInput(deviceId: string): void {
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallAudioInput(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will not change the device that a potential call uses. The call will
|
||||
* need to be ended and started again for this change to take effect
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
public setVideoInput(deviceId: string): void {
|
||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallVideoInput(deviceId);
|
||||
}
|
||||
|
||||
public static getAudioOutput(): string {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
||||
}
|
||||
|
||||
public static getAudioInput(): string {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
||||
}
|
||||
|
||||
public static getVideoInput(): string {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
||||
}
|
||||
}
|
|
@ -385,7 +385,7 @@ export class ModalManager {
|
|||
</div>
|
||||
);
|
||||
|
||||
ReactDOM.render(dialog, ModalManager.getOrCreateContainer());
|
||||
setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()));
|
||||
} else {
|
||||
// This is safe to call repeatedly if we happen to do that
|
||||
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2015 - 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.
|
||||
|
@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Given a room object, return the alias we should use for it,
|
||||
|
@ -25,11 +27,11 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
|||
* @param {Object} room The room object
|
||||
* @returns {string} A display alias for the given room
|
||||
*/
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
export function getDisplayAliasForRoom(room: Room): string {
|
||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||
}
|
||||
|
||||
export function looksLikeDirectMessageRoom(room, myUserId) {
|
||||
export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean {
|
||||
const myMembership = room.getMyMembership();
|
||||
const me = room.getMember(myUserId);
|
||||
|
||||
|
@ -48,7 +50,7 @@ export function looksLikeDirectMessageRoom(room, myUserId) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function guessAndSetDMRoom(room, isDirect) {
|
||||
export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise<void> {
|
||||
let newTarget;
|
||||
if (isDirect) {
|
||||
const guessedUserId = guessDMRoomTargetId(
|
||||
|
@ -70,7 +72,7 @@ export function guessAndSetDMRoom(room, isDirect) {
|
|||
this room as a DM room
|
||||
* @returns {object} A promise
|
||||
*/
|
||||
export function setDMRoom(roomId, userId) {
|
||||
export function setDMRoom(roomId: string, userId: string): Promise<void> {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -114,7 +116,7 @@ export function setDMRoom(roomId, userId) {
|
|||
* @param {string} myUserId User ID of the current user
|
||||
* @returns {string} User ID of the user that the room is probably a DM with
|
||||
*/
|
||||
function guessDMRoomTargetId(room, myUserId) {
|
||||
function guessDMRoomTargetId(room: Room, myUserId: string): string {
|
||||
let oldestTs;
|
||||
let oldestUser;
|
||||
|
|
@ -468,7 +468,7 @@ function restoreEncryptionInfo(searchResultSlice = []) {
|
|||
ev.event.curve25519Key,
|
||||
ev.event.ed25519Key,
|
||||
);
|
||||
ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain;
|
||||
ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain;
|
||||
|
||||
delete ev.event.curve25519Key;
|
||||
delete ev.event.ed25519Key;
|
||||
|
|
|
@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import * as React from 'react';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
|
@ -150,6 +150,10 @@ function success(promise?: Promise<any>) {
|
|||
return {promise};
|
||||
}
|
||||
|
||||
function successSync(value: any) {
|
||||
return success(Promise.resolve(value));
|
||||
}
|
||||
|
||||
/* Disable the "unexpected this" error for these commands - all of the run
|
||||
* functions are called with `this` bound to the Command instance.
|
||||
*/
|
||||
|
@ -160,7 +164,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
description: _td('Sends the given message as a spoiler'),
|
||||
runFn: function(roomId, message) {
|
||||
return success(ContentHelpers.makeHtmlMessage(
|
||||
return successSync(ContentHelpers.makeHtmlMessage(
|
||||
message,
|
||||
`<span data-mx-spoiler>${message}</span>`,
|
||||
));
|
||||
|
@ -176,7 +180,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
return successSync(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -189,7 +193,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
return successSync(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -202,7 +206,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
return successSync(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -215,7 +219,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
return successSync(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -224,7 +228,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
description: _td('Sends a message as plain text, without interpreting it as markdown'),
|
||||
runFn: function(roomId, messages) {
|
||||
return success(ContentHelpers.makeTextMessage(messages));
|
||||
return successSync(ContentHelpers.makeTextMessage(messages));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -233,7 +237,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
description: _td('Sends a message as html, without interpreting it as markdown'),
|
||||
runFn: function(roomId, messages) {
|
||||
return success(ContentHelpers.makeHtmlMessage(messages, messages));
|
||||
return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -978,7 +982,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
runFn: function(roomId, args) {
|
||||
if (!args) return reject(this.getUserId());
|
||||
return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
|
||||
return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -988,7 +992,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
runFn: function(roomId, args) {
|
||||
if (!args) return reject(this.getUserId());
|
||||
return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
|
||||
return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -1015,9 +1019,8 @@ export const Commands = [
|
|||
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
||||
dis.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the
|
||||
// receiver wants.
|
||||
member: member || {userId},
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId } as User,
|
||||
});
|
||||
return success();
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2015 - 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.
|
||||
|
@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import shouldHideEvent from './shouldHideEvent';
|
||||
import {haveTileForEvent} from "./components/views/rooms/EventTile";
|
||||
import { haveTileForEvent } from "./components/views/rooms/EventTile";
|
||||
|
||||
/**
|
||||
* Returns true iff this event arriving in a room should affect the room's
|
||||
|
@ -25,28 +29,33 @@ import {haveTileForEvent} from "./components/views/rooms/EventTile";
|
|||
* @param {Object} ev The event
|
||||
* @returns {boolean} True if the given event should affect the unread message count
|
||||
*/
|
||||
export function eventTriggersUnreadCount(ev) {
|
||||
export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.member') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.third_party_invite') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.server_acl') {
|
||||
return false;
|
||||
} else if (ev.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (ev.getType()) {
|
||||
case EventType.RoomMember:
|
||||
case EventType.RoomThirdPartyInvite:
|
||||
case EventType.CallAnswer:
|
||||
case EventType.CallHangup:
|
||||
case EventType.RoomAliases:
|
||||
case EventType.RoomCanonicalAlias:
|
||||
case EventType.RoomServerAcl:
|
||||
return false;
|
||||
|
||||
case EventType.RoomMessage:
|
||||
if (ev.getContent().msgtype === MsgType.Notice) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ev.isRedacted()) return false;
|
||||
return haveTileForEvent(ev);
|
||||
}
|
||||
|
||||
export function doesRoomHaveUnreadMessages(room) {
|
||||
export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
|
||||
// get the most recent read receipt sent by our account.
|
|
@ -57,6 +57,8 @@ export enum Modifiers {
|
|||
|
||||
// Meta-modifier: isMac ? CMD : CONTROL
|
||||
export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL;
|
||||
// Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts
|
||||
export const DIGITS = "digits";
|
||||
|
||||
interface IKeybind {
|
||||
modifiers?: Modifiers[];
|
||||
|
@ -319,6 +321,7 @@ const alternateKeyName: Record<string, string> = {
|
|||
[Key.SPACE]: _td("Space"),
|
||||
[Key.HOME]: _td("Home"),
|
||||
[Key.END]: _td("End"),
|
||||
[DIGITS]: _td("[number]"),
|
||||
};
|
||||
const keyIcon: Record<string, string> = {
|
||||
[Key.ARROW_UP]: "↑",
|
||||
|
|
|
@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {ReactElement} from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import { ReactElement } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
import CommandProvider from './CommandProvider';
|
||||
import CommunityProvider from './CommunityProvider';
|
||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||
|
@ -24,7 +25,7 @@ import RoomProvider from './RoomProvider';
|
|||
import UserProvider from './UserProvider';
|
||||
import EmojiProvider from './EmojiProvider';
|
||||
import NotifProvider from './NotifProvider';
|
||||
import {timeout} from "../utils/promise";
|
||||
import { timeout } from "../utils/promise";
|
||||
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import SpaceProvider from "./SpaceProvider";
|
||||
|
@ -54,13 +55,14 @@ const PROVIDERS = [
|
|||
EmojiProvider,
|
||||
NotifProvider,
|
||||
CommandProvider,
|
||||
CommunityProvider,
|
||||
DuckDuckGoProvider,
|
||||
];
|
||||
|
||||
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
PROVIDERS.push(SpaceProvider);
|
||||
} else {
|
||||
PROVIDERS.push(CommunityProvider);
|
||||
}
|
||||
|
||||
// Providers will get rejected if they take longer than this.
|
||||
|
|
|
@ -15,7 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import { _t } from '../languageHandler';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
|
|
|
@ -17,28 +17,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {uniqBy, sortBy} from "lodash";
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import { uniqBy, sortBy } from "lodash";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
import { PillCompletion } from './Components';
|
||||
import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
|
||||
import { ICompletion, ISelectionRange } from "./Autocompleter";
|
||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
const ROOM_REGEX = /\B#\S*/g;
|
||||
|
||||
function score(query: string, space: string) {
|
||||
const index = space.indexOf(query);
|
||||
if (index === -1) {
|
||||
return Infinity;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
// Prefer canonical aliases over non-canonical ones
|
||||
function canonicalScore(displayedAlias: string, room: Room): number {
|
||||
return displayedAlias === room.getCanonicalAlias() ? 0 : 1;
|
||||
}
|
||||
|
||||
function matcherObject(room: Room, displayedAlias: string, matchName = "") {
|
||||
|
@ -106,7 +102,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString, limit);
|
||||
completions = sortBy(completions, [
|
||||
(c) => score(matchedString, c.displayedAlias),
|
||||
(c) => canonicalScore(c.displayedAlias, c.room),
|
||||
(c) => c.displayedAlias.length,
|
||||
]);
|
||||
completions = uniqBy(completions, (match) => match.room);
|
||||
|
|
|
@ -20,19 +20,19 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {PillCompletion} from './Components';
|
||||
import { PillCompletion } from './Components';
|
||||
import * as sdk from '../index';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {sortBy} from 'lodash';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { sortBy } from 'lodash';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import RoomMember from "matrix-js-sdk/src/models/room-member";
|
||||
import RoomState from "matrix-js-sdk/src/models/room-state";
|
||||
import EventTimeline from "matrix-js-sdk/src/models/event-timeline";
|
||||
import {makeUserPermalink} from "../utils/permalinks/Permalinks";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||
import { makeUserPermalink } from "../utils/permalinks/Permalinks";
|
||||
import { ICompletion, ISelectionRange } from "./Autocompleter";
|
||||
|
||||
const USER_REGEX = /\B@\S*/g;
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
onScroll?: () => void;
|
||||
onWheel?: () => void;
|
||||
|
@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
|
||||
|
||||
return (<div
|
||||
{...otherProps}
|
||||
ref={this.containerRef}
|
||||
style={this.props.style}
|
||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||
onWheel={this.props.onWheel}
|
||||
tabIndex={this.props.tabIndex}
|
||||
style={style}
|
||||
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||
onWheel={onWheel}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{ this.props.children }
|
||||
{ children }
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import * as sdk from '../../index';
|
|||
import dis from '../../dispatcher/dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onMouseDown = e => {
|
||||
onClick = e => {
|
||||
// only dispatch if its not a no-op
|
||||
if (this.state.selectedTags.length > 0) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
|
@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component {
|
|||
return <div className={classes} onClick={this.onClearFilterClick}>
|
||||
<AutoHideScrollbar
|
||||
className="mx_GroupFilterPanel_scroller"
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
|
||||
onMouseDown={this.onMouseDown}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
<Droppable
|
||||
droppableId="tag-panel-droppable"
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_GroupFilterPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ this.renderGlobalIcon() }
|
||||
{ tags }
|
||||
<div>
|
||||
{createButton}
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
<div className="mx_GroupFilterPanel_tagTileContainer">
|
||||
{ this.renderGlobalIcon() }
|
||||
{ tags }
|
||||
<div>
|
||||
{ createButton }
|
||||
</div>
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
||||
|
||||
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
||||
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
||||
const leftOverflowIndicator = this.props.trackHorizontalOverflow
|
||||
const leftOverflowIndicator = trackHorizontalOverflow
|
||||
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
||||
const rightOverflowIndicator = this.props.trackHorizontalOverflow
|
||||
const rightOverflowIndicator = trackHorizontalOverflow
|
||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||
|
||||
return (<AutoHideScrollbar
|
||||
ref={this._collectScrollerComponent}
|
||||
wrappedRef={this._collectScroller}
|
||||
onWheel={this.onMouseWheel}
|
||||
{...this.props}
|
||||
{...otherProps}
|
||||
>
|
||||
{ leftOverflowIndicator }
|
||||
{ this.props.children }
|
||||
{ children }
|
||||
{ rightOverflowIndicator }
|
||||
</AutoHideScrollbar>);
|
||||
}
|
||||
|
|
|
@ -19,19 +19,16 @@ limitations under the License.
|
|||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import {Key} from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import MediaDeviceHandler from '../../MediaDeviceHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||
import {Resizer, CollapseDistributor} from '../../resizer';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
@ -170,7 +167,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
MediaDeviceHandler.loadDevices();
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
|
@ -569,50 +566,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
_onDragEnd = (result) => {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dest = result.destination.droppableId;
|
||||
|
||||
if (dest === 'tag-panel-droppable') {
|
||||
// Could be "GroupTile +groupId:domain"
|
||||
const draggableId = result.draggableId.split(' ').pop();
|
||||
|
||||
// Dispatch synchronously so that the GroupFilterPanel receives an
|
||||
// optimistic update from GroupFilterOrderStore before the previous
|
||||
// state is shown.
|
||||
dis.dispatch(TagOrderActions.moveTag(
|
||||
this._matrixClient,
|
||||
draggableId,
|
||||
result.destination.index,
|
||||
), true);
|
||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||
this._onRoomTileEndDrag(result);
|
||||
}
|
||||
};
|
||||
|
||||
_onRoomTileEndDrag = (result) => {
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
if (prevTag === 'undefined') prevTag = undefined;
|
||||
|
||||
const roomId = result.draggableId.split('_')[1];
|
||||
|
||||
const oldIndex = result.source.index;
|
||||
const newIndex = result.destination.index;
|
||||
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
this._matrixClient,
|
||||
this._matrixClient.getRoom(roomId),
|
||||
prevTag, newTag,
|
||||
oldIndex, newIndex,
|
||||
), true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
const UserView = sdk.getComponent('structures.UserView');
|
||||
|
@ -679,17 +632,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
aria-hidden={this.props.hideToSRUsers}
|
||||
>
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
||||
<LeftPanel
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
||||
<LeftPanel
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
</div>
|
||||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
|
|
|
@ -1461,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
|
||||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
|
||||
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
|
||||
}, (errorCode) => {
|
||||
// Map JS-SDK error codes to tracker codes for aggregation
|
||||
|
|
|
@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
|
|||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
"To set up a filter, drag a community avatar over to the filter panel on " +
|
||||
"the far left hand side of the screen. You can click on an avatar in the " +
|
||||
"You can click on an avatar in the " +
|
||||
"filter panel at any time to see only the rooms and people associated " +
|
||||
"with that community.",
|
||||
) }
|
||||
|
|
|
@ -337,11 +337,10 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
||||
// If room was shift-clicked, remove it from the room directory
|
||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||
ev.preventDefault();
|
||||
this.removeFromDirectory(room);
|
||||
} else {
|
||||
this.showRoom(room);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -568,11 +567,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
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}_avatar` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_avatar` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<BaseAvatar
|
||||
|
@ -584,42 +583,50 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
url={avatarUrl}
|
||||
/>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_description` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_description` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
<div
|
||||
className="mx_RoomDirectory_name"
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
>
|
||||
{ name }
|
||||
</div>
|
||||
<div
|
||||
className="mx_RoomDirectory_topic"
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
dangerouslySetInnerHTML={{ __html: topic }}
|
||||
/>
|
||||
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
||||
<div
|
||||
className="mx_RoomDirectory_alias"
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
>
|
||||
{ getDisplayAliasForRoom(room) }
|
||||
</div>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_memberCount` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_memberCount` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_preview` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
<div
|
||||
key={ `${room.room_id}_preview` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_preview"
|
||||
>
|
||||
{previewButton}
|
||||
{ previewButton }
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_join` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_join` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
{joinOrViewButton}
|
||||
{ joinOrViewButton }
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export function getUnsentMessages(room) {
|
|||
}
|
||||
|
||||
@replaceableComponent("structures.RoomStatusBar")
|
||||
export default class RoomStatusBar extends React.Component {
|
||||
export default class RoomStatusBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// the room this statusbar is representing.
|
||||
room: PropTypes.object.isRequired,
|
||||
|
|
|
@ -23,8 +23,9 @@ limitations under the License.
|
|||
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
|
@ -59,7 +60,7 @@ import ScrollPanel from "./ScrollPanel";
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import SearchBar from "../views/rooms/SearchBar";
|
||||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
|
@ -80,7 +81,6 @@ import { objectHasDiff } from "../../utils/objects";
|
|||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { omit } from 'lodash';
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
const DEBUG = false;
|
||||
|
@ -139,11 +139,11 @@ export interface IState {
|
|||
draggingFile: boolean;
|
||||
searching: boolean;
|
||||
searchTerm?: string;
|
||||
searchScope?: "All" | "Room";
|
||||
searchScope?: SearchScope;
|
||||
searchResults?: XOR<{}, {
|
||||
count: number;
|
||||
highlights: string[];
|
||||
results: MatrixEvent[];
|
||||
results: SearchResult[];
|
||||
next_batch: string; // eslint-disable-line camelcase
|
||||
}>;
|
||||
searchHighlights?: string[];
|
||||
|
@ -172,11 +172,7 @@ export interface IState {
|
|||
// We load this later by asking the js-sdk to suggest a version for us.
|
||||
// This object is the result of Room#getRecommendedVersion()
|
||||
|
||||
upgradeRecommendation?: {
|
||||
version: string;
|
||||
needsUpgrade: boolean;
|
||||
urgent: boolean;
|
||||
};
|
||||
upgradeRecommendation?: IRecommendedVersion;
|
||||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
layout: Layout;
|
||||
|
@ -572,16 +568,12 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
||||
|
||||
// React only shallow comparison and we only want to trigger
|
||||
// a component re-render if a room requires an upgrade
|
||||
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
|
||||
|
||||
const state = omit(this.state, ['upgradeRecommendation']);
|
||||
const newState = omit(nextState, ['upgradeRecommendation'])
|
||||
const { upgradeRecommendation, ...state } = this.state;
|
||||
const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
|
||||
|
||||
const hasStateDiff =
|
||||
objectHasDiff(state, newState) ||
|
||||
(newUpgradeRecommendation.needsUpgrade === true)
|
||||
newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
|
||||
objectHasDiff(state, newState);
|
||||
|
||||
return hasPropsDiff || hasStateDiff;
|
||||
}
|
||||
|
@ -701,16 +693,11 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
room_id: this.state.room.roomId,
|
||||
event_id: this.state.initialEventId,
|
||||
highlighted: false,
|
||||
replyingToEvent: this.state.replyToEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onLayoutChange = () => {
|
||||
this.setState({
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
});
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
|
@ -1276,7 +1263,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private onSearch = (term: string, scope) => {
|
||||
private onSearch = (term: string, scope: SearchScope) => {
|
||||
this.setState({
|
||||
searchTerm: term,
|
||||
searchScope: scope,
|
||||
|
@ -1297,7 +1284,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.searchId = new Date().getTime();
|
||||
|
||||
let roomId;
|
||||
if (scope === "Room") roomId = this.state.room.roomId;
|
||||
if (scope === SearchScope.Room) roomId = this.state.room.roomId;
|
||||
|
||||
debuglog("sending search request");
|
||||
const searchPromise = eventSearch(term, roomId);
|
||||
|
@ -1644,29 +1631,27 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
let auxPanelMaxHeight = UIStore.instance.windowHeight -
|
||||
(54 + // height of RoomHeader
|
||||
36 + // height of the status area
|
||||
51 + // minimum height of the message compmoser
|
||||
51 + // minimum height of the message composer
|
||||
120); // amount of desired scrollback
|
||||
|
||||
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||
// but it's better than the video going missing entirely
|
||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||
|
||||
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
|
||||
if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
|
||||
this.setState({ auxPanelMaxHeight });
|
||||
}
|
||||
};
|
||||
|
||||
private onStatusBarVisible = () => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: true,
|
||||
});
|
||||
if (this.unmounted || this.state.statusBarVisible) return;
|
||||
this.setState({ statusBarVisible: true });
|
||||
};
|
||||
|
||||
private onStatusBarHidden = () => {
|
||||
// This is currently not desired as it is annoying if it keeps expanding and collapsing
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: false,
|
||||
});
|
||||
if (this.unmounted || !this.state.statusBarVisible) return;
|
||||
this.setState({ statusBarVisible: false });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -2069,7 +2054,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
||||
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||
jumpToBottom = (<JumpToBottomButton
|
||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
roomId={this.state.roomId}
|
||||
|
|
|
@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {ReactNode, useMemo, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
import React, { ReactNode, useMemo, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import classNames from "classnames";
|
||||
import {sortBy} from "lodash";
|
||||
import { sortBy } from "lodash";
|
||||
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import { _t } from "../../languageHandler";
|
||||
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import SearchBox from "./SearchBox";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
||||
import {EnhancedMap} from "../../utils/maps";
|
||||
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||
import { EnhancedMap } from "../../utils/maps";
|
||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import {getOrder} from "../../stores/SpaceStore";
|
||||
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||
import { getChildOrder } from "../../stores/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import {linkifyElement} from "../../HtmlUtils";
|
||||
import { linkifyElement } from "../../HtmlUtils";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
|
@ -286,7 +286,7 @@ export const HierarchyLevel = ({
|
|||
const children = Array.from(relations.get(spaceId)?.values() || []);
|
||||
const sortedChildren = sortBy(children, ev => {
|
||||
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
||||
return getOrder(ev.content.order, null, ev.state_key);
|
||||
return getChildOrder(ev.content.order, null, ev.state_key);
|
||||
});
|
||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||
const roomId = ev.state_key;
|
||||
|
|
|
@ -59,7 +59,7 @@ import IconizedContextMenu, {
|
|||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import {BetaPill} from "../views/beta/BetaCard";
|
||||
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
|
@ -166,7 +166,7 @@ const SpaceInfo = ({ space }) => {
|
|||
const onBetaClick = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: USER_LABS_TAB,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -18,14 +18,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {LayoutPropType} from "../../settings/Layout";
|
||||
import React, {createRef} from 'react';
|
||||
import { LayoutPropType } from "../../settings/Layout";
|
||||
import React, { createRef } from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
|
||||
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
||||
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
|
||||
import { _t } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import UserActivity from "../../UserActivity";
|
||||
import Modal from "../../Modal";
|
||||
|
@ -35,10 +35,11 @@ import { Key } from '../../Keyboard';
|
|||
import Timer from '../../utils/Timer';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import { UIFeature } from "../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
|
@ -439,21 +440,42 @@ class TimelinePanel extends React.Component {
|
|||
};
|
||||
|
||||
onAction = payload => {
|
||||
if (payload.action === 'ignore_state_changed') {
|
||||
this.forceUpdate();
|
||||
}
|
||||
if (payload.action === "edit_event") {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({editState}, () => {
|
||||
if (payload.event && this._messagePanel.current) {
|
||||
this._messagePanel.current.scrollToEventIfNeeded(
|
||||
payload.event.getId(),
|
||||
);
|
||||
switch (payload.action) {
|
||||
case "ignore_state_changed":
|
||||
this.forceUpdate();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({editState}, () => {
|
||||
if (payload.event && this._messagePanel.current) {
|
||||
this._messagePanel.current.scrollToEventIfNeeded(
|
||||
payload.event.getId(),
|
||||
);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case Action.ComposerInsert: {
|
||||
// re-dispatch to the correct composer
|
||||
if (this.state.editState) {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "edit_composer_insert",
|
||||
});
|
||||
} else {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "send_composer_insert",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (payload.action === "scroll_to_bottom") {
|
||||
this.jumpToLiveTimeline();
|
||||
break;
|
||||
}
|
||||
|
||||
case "scroll_to_bottom":
|
||||
this.jumpToLiveTimeline();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import { ActionPayload } from "../../dispatcher/payloads";
|
|||
import { Action } from "../../dispatcher/actions";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ContextMenuButton } from "./ContextMenu";
|
||||
import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
import Modal from "../../Modal";
|
||||
|
@ -408,12 +408,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notification settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
|
|
|
@ -55,7 +55,7 @@ export default class ViewSource extends React.Component {
|
|||
viewSourceContent() {
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||
const isEncrypted = mxEvent.isEncrypted();
|
||||
const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
|
||||
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
||||
const originalEventSource = mxEvent.event;
|
||||
|
||||
if (isEncrypted) {
|
||||
|
|
|
@ -18,14 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import {
|
||||
SetupEncryptionStore,
|
||||
PHASE_LOADING,
|
||||
PHASE_INTRO,
|
||||
PHASE_BUSY,
|
||||
PHASE_DONE,
|
||||
PHASE_CONFIRM_SKIP,
|
||||
} from '../../../stores/SetupEncryptionStore';
|
||||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -61,18 +54,18 @@ export default class CompleteSecurity extends React.Component {
|
|||
let icon;
|
||||
let title;
|
||||
|
||||
if (phase === PHASE_LOADING) {
|
||||
if (phase === Phase.Loading) {
|
||||
return null;
|
||||
} else if (phase === PHASE_INTRO) {
|
||||
} else if (phase === Phase.Intro) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else if (phase === PHASE_DONE) {
|
||||
} else if (phase === Phase.Done) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("Session verified");
|
||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||
} else if (phase === Phase.ConfirmSkip) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Are you sure?");
|
||||
} else if (phase === PHASE_BUSY) {
|
||||
} else if (phase === Phase.Busy) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else {
|
||||
|
|
|
@ -269,7 +269,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private onUIAuthFinished = async (success, response, extra) => {
|
||||
private onUIAuthFinished = async (success: boolean, response: any) => {
|
||||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
|
|
|
@ -21,15 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import Modal from '../../../Modal';
|
||||
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
|
||||
import * as sdk from '../../../index';
|
||||
import {
|
||||
SetupEncryptionStore,
|
||||
PHASE_LOADING,
|
||||
PHASE_INTRO,
|
||||
PHASE_BUSY,
|
||||
PHASE_DONE,
|
||||
PHASE_CONFIRM_SKIP,
|
||||
PHASE_FINISHED,
|
||||
} from '../../../stores/SetupEncryptionStore';
|
||||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
function keyHasPassphrase(keyInfo) {
|
||||
|
@ -63,7 +55,7 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
|
||||
_onStoreUpdate = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
if (store.phase === PHASE_FINISHED) {
|
||||
if (store.phase === Phase.Finished) {
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
@ -136,7 +128,7 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
onClose={this.props.onFinished}
|
||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||
/>;
|
||||
} else if (phase === PHASE_INTRO) {
|
||||
} else if (phase === Phase.Intro) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
|
@ -174,7 +166,7 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_DONE) {
|
||||
} else if (phase === Phase.Done) {
|
||||
let message;
|
||||
if (this.state.backupInfo) {
|
||||
message = <p>{_t(
|
||||
|
@ -200,7 +192,7 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||
} else if (phase === Phase.ConfirmSkip) {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t(
|
||||
|
@ -224,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
|
||||
} else if (phase === Phase.Busy || phase === Phase.Loading) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <Spinner />;
|
||||
} else {
|
||||
|
|
|
@ -17,16 +17,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { toPx } from "../../../utils/units";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -15,10 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
export interface IProps {
|
||||
groupId?: string;
|
||||
|
|
|
@ -16,14 +16,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||
member: RoomMember;
|
||||
|
|
|
@ -13,17 +13,17 @@ 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, {ComponentProps} from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
|
|
|
@ -25,6 +25,7 @@ import TextWithTooltip from "../elements/TextWithTooltip";
|
|||
import Modal from "../../../Modal";
|
||||
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
|
@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
|||
const info = SettingsStore.getBetaInfo(featureId);
|
||||
if (!info) return null; // Beta is invalid/disabled
|
||||
|
||||
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info;
|
||||
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
|
||||
const value = SettingsStore.getValue(featureId);
|
||||
|
||||
let feedbackButton;
|
||||
|
@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
|||
}
|
||||
|
||||
return <div className="mx_BetaCard">
|
||||
<div>
|
||||
<h3 className="mx_BetaCard_title">
|
||||
{ titleOverride || _t(title) }
|
||||
<BetaPill />
|
||||
</h3>
|
||||
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
|
||||
<div className="mx_BetaCard_columns">
|
||||
<div>
|
||||
{ feedbackButton }
|
||||
<AccessibleButton
|
||||
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
|
||||
kind={feedbackButton ? "primary_outline" : "primary"}
|
||||
>
|
||||
{ value ? _t("Leave the beta") : _t("Join the beta") }
|
||||
</AccessibleButton>
|
||||
<h3 className="mx_BetaCard_title">
|
||||
{ titleOverride || _t(title) }
|
||||
<BetaPill />
|
||||
</h3>
|
||||
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
|
||||
<div className="mx_BetaCard_buttons">
|
||||
{ feedbackButton }
|
||||
<AccessibleButton
|
||||
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
|
||||
kind={feedbackButton ? "primary_outline" : "primary"}
|
||||
>
|
||||
{ value ? _t("Leave the beta") : _t("Join the beta") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ disclaimer && <div className="mx_BetaCard_disclaimer">
|
||||
{ disclaimer(value) }
|
||||
</div> }
|
||||
</div>
|
||||
{ disclaimer && <div className="mx_BetaCard_disclaimer">
|
||||
{ disclaimer(value) }
|
||||
</div> }
|
||||
<img src={image} alt="" />
|
||||
</div>
|
||||
<img src={image} alt="" />
|
||||
{ extraSettings && <div className="mx_BetaCard_relatedSettings">
|
||||
{ extraSettings.map(key => (
|
||||
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
|
||||
)) }
|
||||
</div> }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import Field from "../elements/Field";
|
||||
import Dialpad from '../voip/DialPad';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -44,13 +45,21 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
|||
this.setState({value: this.state.value + digit});
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.setState({value: ev.target.value});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <ContextMenu {...this.props}>
|
||||
<div className="mx_DialPadContextMenu_header">
|
||||
<div>
|
||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div>
|
||||
<Field className="mx_DialPadContextMenu_dialled"
|
||||
value={this.state.value} autoFocus={true}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_horizSep" />
|
||||
<div className="mx_DialPadContextMenu_dialPad">
|
||||
|
|
|
@ -33,6 +33,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
export function canCancel(eventStatus) {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
|
@ -178,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
pinnedIds.push(eventId);
|
||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
||||
...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
|
||||
eventId,
|
||||
],
|
||||
});
|
||||
|
@ -200,7 +201,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
|
||||
onQuoteClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'quote',
|
||||
action: Action.ComposerInsert,
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
|
|
|
@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions';
|
|||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
|
||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
|
||||
this._onRemoveClick = this._onRemoveClick.bind(this);
|
||||
}
|
||||
|
||||
_onViewCommunityClick() {
|
||||
_onViewCommunityClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.tag,
|
||||
});
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
_onRemoveClick() {
|
||||
_onRemoveClick = () => {
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
_onMoveUp = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onMoveDown = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
render() {
|
||||
let moveUp;
|
||||
let moveDown;
|
||||
if (this.props.index > 0) {
|
||||
moveUp = (
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
|
||||
{ _t("Move up") }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
|
||||
moveDown = (
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
|
||||
{ _t("Move down") }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||
{ _t('View Community') }
|
||||
</MenuItem>
|
||||
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" role="separator" /> : null }
|
||||
{ moveUp }
|
||||
{ moveDown }
|
||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||
{ _t('Hide') }
|
||||
{ _t("Unpin") }
|
||||
</MenuItem>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -39,6 +39,9 @@ import ProgressBar from "../elements/ProgressBar";
|
|||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
} : null;
|
||||
|
||||
const [truncateAt, setTruncateAt] = useState(20);
|
||||
function overflowTile(overflowCount, totalCount) {
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={() => setTruncateAt(totalCount)} />
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_AddExistingToSpace">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
|
@ -216,16 +230,21 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
{ rooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
|
|
|
@ -15,39 +15,41 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
unknownProfileUsers: Array<{
|
||||
userId: string;
|
||||
errorText: string;
|
||||
}>;
|
||||
onInviteAnyways: () => void;
|
||||
onGiveUp: () => void;
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
|
||||
export default class AskInviteAnywayDialog extends React.Component {
|
||||
static propTypes = {
|
||||
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
|
||||
onInviteAnyways: PropTypes.func.isRequired,
|
||||
onGiveUp: PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onInviteClicked = () => {
|
||||
export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
||||
private onInviteClicked = (): void => {
|
||||
this.props.onInviteAnyways();
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onInviteNeverWarnClicked = () => {
|
||||
private onInviteNeverWarnClicked = (): void => {
|
||||
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
|
||||
this.props.onInviteAnyways();
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onGiveUpClicked = () => {
|
||||
private onGiveUpClicked = (): void => {
|
||||
this.props.onGiveUp();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const errorList = this.props.unknownProfileUsers
|
||||
|
@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component {
|
|||
|
||||
return (
|
||||
<BaseDialog className='mx_RetryInvitesDialog'
|
||||
onFinished={this._onGiveUpClicked}
|
||||
onFinished={this.onGiveUpClicked}
|
||||
title={_t('The following users may not exist')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
{/* eslint-disable-next-line */}
|
||||
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
|
||||
<ul>
|
||||
{ errorList }
|
||||
|
@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
|
|||
</div>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onGiveUpClicked}>
|
||||
<button onClick={this.onGiveUpClicked}>
|
||||
{ _t('Close') }
|
||||
</button>
|
||||
<button onClick={this._onInviteNeverWarnClicked}>
|
||||
<button onClick={this.onInviteNeverWarnClicked}>
|
||||
{ _t('Invite anyway and never warn me again') }
|
||||
</button>
|
||||
<button onClick={this._onInviteClicked} autoFocus={true}>
|
||||
<button onClick={this.onInviteClicked} autoFocus={true}>
|
||||
{ _t('Invite anyway') }
|
||||
</button>
|
||||
</div>
|
|
@ -29,7 +29,7 @@ import InfoDialog from "./InfoDialog";
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {USER_LABS_TAB} from "./UserSettingsDialog";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
featureId: string;
|
||||
|
@ -44,7 +44,12 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
|
|||
const sendFeedback = async (ok: boolean) => {
|
||||
if (!ok) return onFinished(false);
|
||||
|
||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact);
|
||||
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
|
||||
o[k] = SettingsStore.getValue(k);
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
|
||||
onFinished(true);
|
||||
|
||||
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
|
||||
|
@ -70,7 +75,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
|
|||
onFinished(false);
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: USER_LABS_TAB,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
}}>
|
||||
{ _t("To leave the beta, visit your settings.") }
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -27,8 +26,27 @@ import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-ragesh
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
initialText?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
sendLogs: boolean;
|
||||
busy: boolean;
|
||||
err: string;
|
||||
issueUrl: string;
|
||||
text: string;
|
||||
progress: string;
|
||||
downloadBusy: boolean;
|
||||
downloadProgress: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.BugReportDialog")
|
||||
export default class BugReportDialog extends React.Component {
|
||||
export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||
private unmounted: boolean;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component {
|
|||
downloadBusy: false,
|
||||
downloadProgress: null,
|
||||
};
|
||||
this._unmounted = false;
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onTextChange = this._onTextChange.bind(this);
|
||||
this._onIssueUrlChange = this._onIssueUrlChange.bind(this);
|
||||
this._onSendLogsChange = this._onSendLogsChange.bind(this);
|
||||
this._sendProgressCallback = this._sendProgressCallback.bind(this);
|
||||
this._downloadProgressCallback = this._downloadProgressCallback.bind(this);
|
||||
this.unmounted = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
_onCancel(ev) {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onSubmit(ev) {
|
||||
private onSubmit = (): void => {
|
||||
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
|
||||
this.setState({
|
||||
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
|
||||
|
@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component {
|
|||
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
|
||||
|
||||
this.setState({ busy: true, progress: null, err: null });
|
||||
this._sendProgressCallback(_t("Preparing to send logs"));
|
||||
this.sendProgressCallback(_t("Preparing to send logs"));
|
||||
|
||||
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText,
|
||||
sendLogs: true,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
progressCallback: this.sendProgressCallback,
|
||||
label: this.props.label,
|
||||
}).then(() => {
|
||||
if (!this._unmounted) {
|
||||
if (!this.unmounted) {
|
||||
this.props.onFinished(false);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
||||
|
@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
}, (err) => {
|
||||
if (!this._unmounted) {
|
||||
if (!this.unmounted) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
progress: null,
|
||||
|
@ -101,14 +112,14 @@ export default class BugReportDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onDownload = async (ev) => {
|
||||
private onDownload = async (): Promise<void> => {
|
||||
this.setState({ downloadBusy: true });
|
||||
this._downloadProgressCallback(_t("Preparing to download logs"));
|
||||
this.downloadProgressCallback(_t("Preparing to download logs"));
|
||||
|
||||
try {
|
||||
await downloadBugReport({
|
||||
sendLogs: true,
|
||||
progressCallback: this._downloadProgressCallback,
|
||||
progressCallback: this.downloadProgressCallback,
|
||||
label: this.props.label,
|
||||
});
|
||||
|
||||
|
@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component {
|
|||
downloadProgress: null,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!this._unmounted) {
|
||||
if (!this.unmounted) {
|
||||
this.setState({
|
||||
downloadBusy: false,
|
||||
downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
|
||||
|
@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onTextChange(ev) {
|
||||
this.setState({ text: ev.target.value });
|
||||
private onTextChange = (ev: React.FormEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({ text: ev.currentTarget.value });
|
||||
}
|
||||
|
||||
_onIssueUrlChange(ev) {
|
||||
this.setState({ issueUrl: ev.target.value });
|
||||
private onIssueUrlChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({ issueUrl: ev.currentTarget.value });
|
||||
}
|
||||
|
||||
_onSendLogsChange(ev) {
|
||||
this.setState({ sendLogs: ev.target.checked });
|
||||
}
|
||||
|
||||
_sendProgressCallback(progress) {
|
||||
if (this._unmounted) {
|
||||
private sendProgressCallback = (progress: string): void => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({progress: progress});
|
||||
this.setState({ progress });
|
||||
}
|
||||
|
||||
_downloadProgressCallback(downloadProgress) {
|
||||
if (this._unmounted) {
|
||||
private downloadProgressCallback = (downloadProgress: string): void => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({ downloadProgress });
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this.onCancel}
|
||||
title={_t('Submit debug logs')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
|
@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {
|
|||
</b></p>
|
||||
|
||||
<div className="mx_BugReportDialog_download">
|
||||
<AccessibleButton onClick={this._onDownload} kind="link" disabled={this.state.downloadBusy}>
|
||||
<AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
|
||||
{ _t("Download logs") }
|
||||
</AccessibleButton>
|
||||
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
|
||||
|
@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component {
|
|||
type="text"
|
||||
className="mx_BugReportDialog_field_input"
|
||||
label={_t("GitHub issue")}
|
||||
onChange={this._onIssueUrlChange}
|
||||
onChange={this.onIssueUrlChange}
|
||||
value={this.state.issueUrl}
|
||||
placeholder="https://github.com/vector-im/element-web/issues/..."
|
||||
/>
|
||||
|
@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component {
|
|||
element="textarea"
|
||||
label={_t("Notes")}
|
||||
rows={5}
|
||||
onChange={this._onTextChange}
|
||||
onChange={this.onTextChange}
|
||||
value={this.state.text}
|
||||
placeholder={_t(
|
||||
"If there is additional context that would help in " +
|
||||
|
@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component {
|
|||
{error}
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Send logs")}
|
||||
onPrimaryButtonClick={this._onSubmit}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
focus={true}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BugReportDialog.propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
initialText: PropTypes.string,
|
||||
};
|
|
@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
interface IProps {
|
||||
newVersion: string;
|
||||
version: string;
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
|
||||
|
||||
export default class ChangelogDialog extends React.Component {
|
||||
export default class ChangelogDialog extends React.Component<IProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount() {
|
||||
const version = this.props.newVersion.split('-');
|
||||
const version2 = this.props.version.split('-');
|
||||
if (version == null || version2 == null) return;
|
||||
|
@ -49,7 +54,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_elementsForCommit(commit) {
|
||||
private elementsForCommit(commit): JSX.Element {
|
||||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||
|
@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
|
||||
|
@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
msg: this.state[repo],
|
||||
});
|
||||
} else {
|
||||
content = this.state[repo].map(this._elementsForCommit);
|
||||
content = this.state[repo].map(this.elementsForCommit);
|
||||
}
|
||||
return (
|
||||
<div key={repo}>
|
||||
|
@ -99,9 +104,3 @@ export default class ChangelogDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangelogDialog.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
newVersion: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
|
@ -17,7 +17,17 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
redact: () => Promise<void>;
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isRedacting: boolean;
|
||||
redactionErrorCode: string | number;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
|
@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
* To avoid this, we keep the dialog open as long as /redact is in progress.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
|
||||
export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
||||
export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -41,7 +51,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
onParentFinished = async (proceed) => {
|
||||
public onParentFinished = async (proceed: boolean): Promise<void> => {
|
||||
if (proceed) {
|
||||
this.setState({isRedacting: true});
|
||||
try {
|
||||
|
@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
if (this.state.isRedacting) {
|
||||
if (this.state.redactionErrorCode) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
@ -19,11 +19,15 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
|
||||
export default class ConfirmRedactDialog extends React.Component {
|
||||
export default class ConfirmRedactDialog extends React.Component<IProps> {
|
||||
render() {
|
||||
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
||||
return (
|
|
@ -15,13 +15,31 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: RoomMember;
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType;
|
||||
// needed if a group member is specified
|
||||
matrixClient?: MatrixClient,
|
||||
action: string; // eg. 'Ban'
|
||||
title: string; // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
// be the string entered.
|
||||
askReason?: boolean;
|
||||
danger?: boolean;
|
||||
onFinished: (success: boolean, reason?: string) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
|
@ -32,53 +50,23 @@ import {mediaFromMxc} from "../../../customisations/Media";
|
|||
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
|
||||
export default class ConfirmUserActionDialog extends React.Component {
|
||||
static propTypes = {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: PropTypes.object,
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType,
|
||||
// needed if a group member is specified
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
action: PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
// be the string entered.
|
||||
askReason: PropTypes.bool,
|
||||
danger: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
||||
private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
static defaultProps = {
|
||||
danger: false,
|
||||
askReason: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._reasonField = null;
|
||||
}
|
||||
|
||||
onOk = () => {
|
||||
let reason;
|
||||
if (this._reasonField) {
|
||||
reason = this._reasonField.value;
|
||||
}
|
||||
this.props.onFinished(true, reason);
|
||||
public onOk = (): void => {
|
||||
this.props.onFinished(true, this.reasonField.current?.value);
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
public onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
_collectReasonField = e => {
|
||||
this._reasonField = e;
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
|
@ -92,7 +80,7 @@ export default class ConfirmUserActionDialog extends React.Component {
|
|||
<div>
|
||||
<form onSubmit={this.onOk}>
|
||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||
ref={this._collectReasonField}
|
||||
ref={this.reasonField}
|
||||
placeholder={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
|
@ -15,22 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
|
||||
export default class ConfirmWipeDeviceDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
_onConfirm = () => {
|
||||
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
|
||||
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
|
||||
private onConfirm = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onDecline = () => {
|
||||
private onDecline = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
|
@ -55,10 +54,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Clear all data")}
|
||||
onPrimaryButtonClick={this._onConfirm}
|
||||
onPrimaryButtonClick={this.onConfirm}
|
||||
primaryButtonClass="danger"
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onDecline}
|
||||
onCancel={this.onDecline}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -15,44 +15,51 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
||||
export default class CreateGroupDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
state = {
|
||||
interface IState {
|
||||
groupName: string;
|
||||
groupId: string;
|
||||
groupIdError: string;
|
||||
creating: boolean;
|
||||
createError: Error;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
||||
export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||
public state = {
|
||||
groupName: '',
|
||||
groupId: '',
|
||||
groupError: null,
|
||||
groupIdError: '',
|
||||
creating: false,
|
||||
createError: null,
|
||||
};
|
||||
|
||||
_onGroupNameChange = e => {
|
||||
private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupName: e.target.value,
|
||||
groupName: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onGroupIdChange = e => {
|
||||
private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupId: e.target.value,
|
||||
groupId: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onGroupIdBlur = e => {
|
||||
this._checkGroupId();
|
||||
private onGroupIdBlur = (): void => {
|
||||
this.checkGroupId();
|
||||
};
|
||||
|
||||
_checkGroupId(e) {
|
||||
private checkGroupId() {
|
||||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
|
@ -67,12 +74,12 @@ export default class CreateGroupDialog extends React.Component {
|
|||
return error;
|
||||
}
|
||||
|
||||
_onFormSubmit = e => {
|
||||
private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this._checkGroupId()) return;
|
||||
if (this.checkGroupId()) return;
|
||||
|
||||
const profile = {};
|
||||
const profile: any = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
|
@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component {
|
|||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
|
@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
|
|||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
autoFocus={true} size="64"
|
||||
autoFocus={true} size={64}
|
||||
placeholder={_t('Example')}
|
||||
onChange={this._onGroupNameChange}
|
||||
onChange={this.onGroupNameChange}
|
||||
value={this.state.groupName}
|
||||
/>
|
||||
</div>
|
||||
|
@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component {
|
|||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size="32"
|
||||
size={32}
|
||||
placeholder={_t('example')}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
onChange={this.onGroupIdChange}
|
||||
onBlur={this.onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
|
@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default (props) => {
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
export default (props: IProps) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
const _onLogoutClicked = () => {
|
||||
|
@ -40,7 +44,7 @@ export default (props) => {
|
|||
onFinished: (doLogout) => {
|
||||
if (doLogout) {
|
||||
dis.dispatch({action: 'logout'});
|
||||
props.onFinished();
|
||||
props.onFinished(true);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv
|
|||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
shouldErase: boolean;
|
||||
errStr: string;
|
||||
authData: any; // for UIA
|
||||
authEnabled: boolean; // see usages for information
|
||||
|
||||
// A few strings that are passed to InteractiveAuth for design or are displayed
|
||||
// next to the InteractiveAuth component.
|
||||
bodyText: string;
|
||||
continueText: string;
|
||||
continueKind: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
continueKind: null,
|
||||
};
|
||||
|
||||
this._initAuth(/* shouldErase= */false);
|
||||
this.initAuth(/* shouldErase= */false);
|
||||
}
|
||||
|
||||
_onStagePhaseChange = (stage, phase) => {
|
||||
private onStagePhaseChange = (stage: string, phase: string): void => {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
|
||||
|
@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this.setState({bodyText, continueText, continueKind});
|
||||
};
|
||||
|
||||
_onUIAuthFinished = (success, result, extra) => {
|
||||
private onUIAuthFinished = (success: boolean, result: Error) => {
|
||||
if (success) return; // great! makeRequest() will be called too.
|
||||
|
||||
if (result === ERROR_USER_CANCELLED) {
|
||||
this._onCancel();
|
||||
this.onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Error during UI Auth:", {result, extra});
|
||||
console.error("Error during UI Auth:", { result });
|
||||
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
||||
};
|
||||
|
||||
_onUIAuthComplete = (auth) => {
|
||||
private onUIAuthComplete = (auth: any): void => {
|
||||
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
|
||||
// Deactivation worked - logout & close this dialog
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
|
@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onEraseFieldChange = (ev) => {
|
||||
private onEraseFieldChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
shouldErase: ev.currentTarget.checked,
|
||||
|
||||
// Disable the auth form because we're going to have to reinitialize the auth
|
||||
// information. We do this because we can't modify the parameters in the UIA
|
||||
|
@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
|
||||
// As mentioned above, set up for auth again to get updated UIA session info
|
||||
this._initAuth(/* shouldErase= */ev.target.checked);
|
||||
this.initAuth(/* shouldErase= */ev.currentTarget.checked);
|
||||
};
|
||||
|
||||
_onCancel() {
|
||||
private onCancel(): void {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_initAuth(shouldErase) {
|
||||
private initAuth(shouldErase: boolean): void {
|
||||
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
|
||||
// If we got here, oops. The server didn't require any auth.
|
||||
// Our application lifecycle will catch the error and do the logout bits.
|
||||
|
@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let error = null;
|
||||
|
@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
<InteractiveAuth
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
authData={this.state.authData}
|
||||
makeRequest={this._onUIAuthComplete}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
onStagePhaseChange={this._onStagePhaseChange}
|
||||
makeRequest={this.onUIAuthComplete}
|
||||
onAuthFinished={this.onUIAuthFinished}
|
||||
onStagePhaseChange={this.onStagePhaseChange}
|
||||
continueText={this.state.continueText}
|
||||
continueKind={this.state.continueKind}
|
||||
/>
|
||||
|
@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
<p>
|
||||
<StyledCheckbox
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
onChange={this.onEraseFieldChange}
|
||||
>
|
||||
{_t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
|
@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeactivateAccountDialog.propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
|
@ -525,11 +525,11 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
|
|||
}
|
||||
|
||||
interface IAccountDataExplorerState {
|
||||
[inputId: string]: boolean | string | any;
|
||||
isRoomAccountData: boolean;
|
||||
event?: MatrixEvent;
|
||||
editing: boolean;
|
||||
queryEventType: string;
|
||||
[inputId: string]: boolean | string;
|
||||
}
|
||||
|
||||
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
|
||||
|
|
|
@ -26,37 +26,37 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.dialogs.ErrorDialog")
|
||||
export default class ErrorDialog extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
title?: string;
|
||||
description?: React.ReactNode;
|
||||
button?: string;
|
||||
focus?: boolean;
|
||||
headerImage?: string;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
interface IState {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.ErrorDialog")
|
||||
export default class ErrorDialog extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
focus: true,
|
||||
title: null,
|
||||
description: null,
|
||||
button: null,
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
private onClick = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog
|
|
@ -14,31 +14,35 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useMemo, useState, useEffect} from "react";
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import classnames from "classnames";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import { useSettingValue, useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import {avatarUrlForUser} from "../../../Avatar";
|
||||
import { avatarUrlForUser } from "../../../Avatar";
|
||||
import EventTile from "../rooms/EventTile";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import {Alignment} from '../elements/Tooltip';
|
||||
import { Alignment } from '../elements/Tooltip';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
||||
const AVATAR_SIZE = 30;
|
||||
|
||||
|
@ -162,6 +166,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
});
|
||||
mockEvent.sender = {
|
||||
name: profileInfo.displayname || userId,
|
||||
rawDisplayName: profileInfo.displayname,
|
||||
userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return avatarUrlForUser(
|
||||
|
@ -170,7 +175,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
);
|
||||
},
|
||||
getMxcAvatarUrl: () => profileInfo.avatar_url,
|
||||
};
|
||||
} as RoomMember;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
@ -194,6 +199,17 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
}).match(lcQuery);
|
||||
}
|
||||
|
||||
const [truncateAt, setTruncateAt] = useState(20);
|
||||
function overflowTile(overflowCount, totalCount) {
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={() => setTruncateAt(totalCount)} />
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Forward message")}
|
||||
className="mx_ForwardDialog"
|
||||
|
@ -226,15 +242,20 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_ForwardList_results">
|
||||
{ rooms.map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
event={event}
|
||||
matrixClient={cli}
|
||||
onFinished={onFinished}
|
||||
/>,
|
||||
) }
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
event={event}
|
||||
matrixClient={cli}
|
||||
onFinished={onFinished}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
) : <span className="mx_ForwardList_noResults">
|
||||
{ _t("No results") }
|
||||
|
|
|
@ -158,8 +158,8 @@ class ThreepidMember extends Member {
|
|||
}
|
||||
|
||||
interface IDMUserTileProps {
|
||||
member: RoomMember;
|
||||
onRemove(member: RoomMember): void;
|
||||
member: Member;
|
||||
onRemove(member: Member): void;
|
||||
}
|
||||
|
||||
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||
|
@ -173,7 +173,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
|
||||
render() {
|
||||
const avatarSize = 20;
|
||||
const avatar = this.props.member.isEmail
|
||||
const avatar = (this.props.member as ThreepidMember).isEmail
|
||||
? <img
|
||||
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
|
@ -215,9 +215,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
}
|
||||
|
||||
interface IDMRoomTileProps {
|
||||
member: RoomMember;
|
||||
member: Member;
|
||||
lastActiveTs: number;
|
||||
onToggle(member: RoomMember): void;
|
||||
onToggle(member: Member): void;
|
||||
highlightWord: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
}
|
||||
|
||||
const avatarSize = 36;
|
||||
const avatar = this.props.member.isEmail
|
||||
const avatar = (this.props.member as ThreepidMember).isEmail
|
||||
? <img
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
|
@ -303,7 +303,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
</span>
|
||||
);
|
||||
|
||||
const caption = this.props.member.isEmail
|
||||
const caption = (this.props.member as ThreepidMember).isEmail
|
||||
? _t("Invite by email")
|
||||
: this.highlightName(this.props.member.userId);
|
||||
|
||||
|
@ -339,7 +339,7 @@ interface IInviteDialogProps {
|
|||
}
|
||||
|
||||
interface IInviteDialogState {
|
||||
targets: RoomMember[]; // array of Member objects (see interface above)
|
||||
targets: Member[]; // array of Member objects (see interface above)
|
||||
filterText: string;
|
||||
recents: { user: Member, userId: string }[];
|
||||
numRecentsShown: number;
|
||||
|
|
|
@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
|
||||
state: IState = {
|
||||
disabledButtonIds: [],
|
||||
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
|
||||
.map(b => b.id),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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, {PureComponent} from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Markdown from '../../../Markdown';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
/*
|
||||
* A dialog for reporting an event.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ReportEventDialog")
|
||||
export default class ReportEventDialog extends PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
reason: "",
|
||||
busy: false,
|
||||
err: null,
|
||||
};
|
||||
}
|
||||
|
||||
_onReasonChange = ({target: {value: reason}}) => {
|
||||
this.setState({ reason });
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
_onSubmit = async () => {
|
||||
if (!this.state.reason || !this.state.reason.trim()) {
|
||||
this.setState({
|
||||
err: _t("Please fill why you're reporting."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
err: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const ev = this.props.mxEvent;
|
||||
await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
err: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Loader = sdk.getComponent('elements.Spinner');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
</div>;
|
||||
}
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const adminMessageMD =
|
||||
SdkConfig.get().reportEvent &&
|
||||
SdkConfig.get().reportEvent.adminMessageMD;
|
||||
let adminMessage;
|
||||
if (adminMessageMD) {
|
||||
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_BugReportDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Report Content to Your Homeserver Administrator')}
|
||||
contentId='mx_ReportEventDialog'
|
||||
>
|
||||
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
|
||||
<p>
|
||||
{
|
||||
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
|
||||
"your homeserver. If messages in this room are encrypted, your homeserver " +
|
||||
"administrator will not be able to read the message text or view any files or images.")
|
||||
}
|
||||
</p>
|
||||
{adminMessage}
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
rows={5}
|
||||
onChange={this._onReasonChange}
|
||||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
onPrimaryButtonClick={this._onSubmit}
|
||||
focus={true}
|
||||
onCancel={this._onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
445
src/components/views/dialogs/ReportEventDialog.tsx
Normal file
445
src/components/views/dialogs/ReportEventDialog.tsx
Normal file
|
@ -0,0 +1,445 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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 * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Markdown from '../../../Markdown';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// A free-form text describing the abuse.
|
||||
reason: string;
|
||||
busy: boolean;
|
||||
err?: string;
|
||||
// If we know it, the nature of the abuse, as specified by MSC3215.
|
||||
nature?: EXTENDED_NATURE;
|
||||
}
|
||||
|
||||
|
||||
const MODERATED_BY_STATE_EVENT_TYPE = [
|
||||
"org.matrix.msc3215.room.moderation.moderated_by",
|
||||
/**
|
||||
* Unprefixed state event. Not ready for prime time.
|
||||
*
|
||||
* "m.room.moderation.moderated_by"
|
||||
*/
|
||||
];
|
||||
|
||||
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
|
||||
|
||||
// Standard abuse natures.
|
||||
enum NATURE {
|
||||
DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
|
||||
TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
|
||||
ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
|
||||
SPAM = "org.matrix.msc3215.abuse.nature.spam",
|
||||
OTHER = "org.matrix.msc3215.abuse.nature.other",
|
||||
}
|
||||
|
||||
enum NON_STANDARD_NATURE {
|
||||
// Non-standard abuse nature.
|
||||
// It should never leave the client - we use it to fallback to
|
||||
// server-wide abuse reporting.
|
||||
ADMIN = "non-standard.abuse.nature.admin"
|
||||
}
|
||||
|
||||
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
|
||||
|
||||
type Moderation = {
|
||||
// The id of the moderation room.
|
||||
moderationRoomId: string;
|
||||
// The id of the bot in charge of forwarding abuse reports to the moderation room.
|
||||
moderationBotUserId: string;
|
||||
}
|
||||
/*
|
||||
* A dialog for reporting an event.
|
||||
*
|
||||
* The actual content of the dialog will depend on two things:
|
||||
*
|
||||
* 1. Is `feature_report_to_moderators` enabled?
|
||||
* 2. Does the room support moderation as per MSC3215, i.e. is there
|
||||
* a well-formed state event `m.room.moderation.moderated_by`
|
||||
* /`org.matrix.msc3215.room.moderation.moderated_by`?
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ReportEventDialog")
|
||||
export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||
// If the room supports moderation, the moderation information.
|
||||
private moderation?: Moderation;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
let moderatedByRoomId = null;
|
||||
let moderatedByUserId = null;
|
||||
|
||||
if (SettingsStore.getValue("feature_report_to_moderators")) {
|
||||
// The client supports reporting to moderators.
|
||||
// Does the room support it, too?
|
||||
|
||||
// Extract state events to determine whether we should display
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(props.mxEvent.getRoomId());
|
||||
|
||||
for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
|
||||
const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType);
|
||||
if (!stateEvent) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(stateEvent)) {
|
||||
// Internal error.
|
||||
throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` +
|
||||
"should return at most one state event");
|
||||
}
|
||||
const event = stateEvent.event;
|
||||
if (!("content" in event) || typeof event["content"] != "object") {
|
||||
// The room is improperly configured.
|
||||
// Display this debug message for the sake of moderators.
|
||||
console.debug("Moderation error", "state event", stateEventType,
|
||||
"should have an object field `content`, got", event);
|
||||
continue;
|
||||
}
|
||||
const content = event["content"];
|
||||
if (!("room_id" in content) || typeof content["room_id"] != "string") {
|
||||
// The room is improperly configured.
|
||||
// Display this debug message for the sake of moderators.
|
||||
console.debug("Moderation error", "state event", stateEventType,
|
||||
"should have a string field `content.room_id`, got", event);
|
||||
continue;
|
||||
}
|
||||
if (!("user_id" in content) || typeof content["user_id"] != "string") {
|
||||
// The room is improperly configured.
|
||||
// Display this debug message for the sake of moderators.
|
||||
console.debug("Moderation error", "state event", stateEventType,
|
||||
"should have a string field `content.user_id`, got", event);
|
||||
continue;
|
||||
}
|
||||
moderatedByRoomId = content["room_id"];
|
||||
moderatedByUserId = content["user_id"];
|
||||
}
|
||||
|
||||
if (moderatedByRoomId && moderatedByUserId) {
|
||||
// The room supports moderation.
|
||||
this.moderation = {
|
||||
moderationRoomId: moderatedByRoomId,
|
||||
moderationBotUserId: moderatedByUserId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
// A free-form text describing the abuse.
|
||||
reason: "",
|
||||
busy: false,
|
||||
err: null,
|
||||
// If specified, the nature of the abuse, as specified by MSC3215.
|
||||
nature: null,
|
||||
};
|
||||
}
|
||||
|
||||
// The user has written down a freeform description of the abuse.
|
||||
private onReasonChange = ({target: {value: reason}}): void => {
|
||||
this.setState({ reason });
|
||||
};
|
||||
|
||||
// The user has clicked on a nature.
|
||||
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE});
|
||||
};
|
||||
|
||||
// The user has clicked "cancel".
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
// The user has clicked "submit".
|
||||
private onSubmit = async () => {
|
||||
let reason = this.state.reason || "";
|
||||
reason = reason.trim();
|
||||
if (this.moderation) {
|
||||
// This room supports moderation.
|
||||
// We need a nature.
|
||||
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
|
||||
if (!this.state.nature ||
|
||||
((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
|
||||
&& !reason)
|
||||
) {
|
||||
this.setState({
|
||||
err: _t("Please fill why you're reporting."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// This room does not support moderation.
|
||||
// We need a `reason`.
|
||||
if (!reason) {
|
||||
this.setState({
|
||||
err: _t("Please fill why you're reporting."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
err: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const client = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
|
||||
const nature: NATURE = this.state.nature;
|
||||
|
||||
// Report to moderators through to the dedicated bot,
|
||||
// as configured in the room's state events.
|
||||
const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
|
||||
await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
|
||||
event_id: ev.getId(),
|
||||
room_id: ev.getRoomId(),
|
||||
moderated_by_id: this.moderation.moderationRoomId,
|
||||
nature,
|
||||
reporter: client.getUserId(),
|
||||
comment: this.state.reason.trim(),
|
||||
});
|
||||
} else {
|
||||
// Report to homeserver admin through the dedicated Matrix API.
|
||||
await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
err: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Loader = sdk.getComponent('elements.Spinner');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
</div>;
|
||||
}
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const adminMessageMD =
|
||||
SdkConfig.get().reportEvent &&
|
||||
SdkConfig.get().reportEvent.adminMessageMD;
|
||||
let adminMessage;
|
||||
if (adminMessageMD) {
|
||||
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
if (this.moderation) {
|
||||
// Display report-to-moderator dialog.
|
||||
// We let the user pick a nature.
|
||||
const client = MatrixClientPeg.get();
|
||||
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
|
||||
let subtitle;
|
||||
switch (this.state.nature) {
|
||||
case NATURE.DISAGREEMENT:
|
||||
subtitle = _t("What this user is writing is wrong.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NATURE.TOXIC:
|
||||
subtitle = _t("This user is displaying toxic behaviour, " +
|
||||
"for instance by insulting other users or sharing " +
|
||||
" adult-only content in a family-friendly room " +
|
||||
" or otherwise violating the rules of this room.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NATURE.ILLEGAL:
|
||||
subtitle = _t("This user is displaying illegal behaviour, " +
|
||||
"for instance by doxing people or threatening violence.\n" +
|
||||
"This will be reported to the room moderators who may escalate this to legal authorities.");
|
||||
break;
|
||||
case NATURE.SPAM:
|
||||
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NON_STANDARD_NATURE.ADMIN:
|
||||
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
|
||||
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||
"This will be reported to the administrators of %(homeserver)s. " +
|
||||
"The administrators will NOT be able to read the encrypted content of this room.",
|
||||
{ homeserver: homeServerName });
|
||||
} else {
|
||||
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||
" This will be reported to the administrators of %(homeserver)s.",
|
||||
{ homeserver: homeServerName });
|
||||
}
|
||||
break;
|
||||
case NATURE.OTHER:
|
||||
subtitle = _t("Any other reason. Please describe the problem.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
default:
|
||||
subtitle = _t("Please pick a nature and describe what makes this message abusive.");
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ReportEventDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Report Content')}
|
||||
contentId='mx_ReportEventDialog'
|
||||
>
|
||||
<div>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.DISAGREEMENT }
|
||||
checked = { this.state.nature == NATURE.DISAGREEMENT }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Disagree')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.TOXIC }
|
||||
checked = { this.state.nature == NATURE.TOXIC }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Toxic Behaviour')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.ILLEGAL }
|
||||
checked = { this.state.nature == NATURE.ILLEGAL }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Illegal Content')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.SPAM }
|
||||
checked = { this.state.nature == NATURE.SPAM }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Spam or propaganda')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NON_STANDARD_NATURE.ADMIN }
|
||||
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Report the entire room')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.OTHER }
|
||||
checked = { this.state.nature == NATURE.OTHER }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Other')}
|
||||
</StyledRadioButton>
|
||||
<p>
|
||||
{subtitle}
|
||||
</p>
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
rows={5}
|
||||
onChange={this.onReasonChange}
|
||||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
focus={true}
|
||||
onCancel={this.onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
// Report to homeserver admin.
|
||||
// Currently, the API does not support natures.
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ReportEventDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Report Content to Your Homeserver Administrator')}
|
||||
contentId='mx_ReportEventDialog'
|
||||
>
|
||||
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
|
||||
<p>
|
||||
{
|
||||
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
|
||||
"your homeserver. If messages in this room are encrypted, your homeserver " +
|
||||
"administrator will not be able to read the message text or view any files " +
|
||||
"or images.")
|
||||
}
|
||||
</p>
|
||||
{adminMessage}
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
rows={5}
|
||||
onChange={this.onReasonChange}
|
||||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
focus={true}
|
||||
onCancel={this.onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
|||
ROOM_ADVANCED_TAB,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
|
||||
<AdvancedRoomSettingsTab
|
||||
roomId={this.props.roomId}
|
||||
closeSettingsFn={() => this.props.onFinished(true)}
|
||||
/>,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import React, { useMemo } from 'react';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DevtoolsDialog from "./DevtoolsDialog";
|
||||
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
|
||||
import {getTopic} from "../elements/RoomTopic";
|
||||
import {avatarUrlForRoom} from "../../../Avatar";
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import TabbedView, { Tab } from "../../structures/TabbedView";
|
||||
import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
|
||||
import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||
|
||||
export enum SpaceSettingsTab {
|
||||
General = "SPACE_GENERAL_TAB",
|
||||
Visibility = "SPACE_VISIBILITY_TAB",
|
||||
Advanced = "SPACE_ADVANCED_TAB",
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
}
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
||||
const avatarChanged = newAvatar !== null;
|
||||
|
||||
const [name, setName] = useState<string>(space.name);
|
||||
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
||||
const nameChanged = name !== space.name;
|
||||
|
||||
const currentTopic = getTopic(space);
|
||||
const [topic, setTopic] = useState<string>(currentTopic);
|
||||
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
||||
const topicChanged = topic !== currentTopic;
|
||||
|
||||
const currentJoinRule = space.getJoinRule();
|
||||
const [joinRule, setJoinRule] = useState(currentJoinRule);
|
||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||
const joinRuleChanged = joinRule !== currentJoinRule;
|
||||
|
||||
const onSave = async () => {
|
||||
setBusy(true);
|
||||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
if (newAvatar) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
} else {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
|
||||
}
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
promises.push(cli.setRoomName(space.roomId, name));
|
||||
}
|
||||
|
||||
if (topicChanged) {
|
||||
promises.push(cli.setRoomTopic(space.roomId, topic));
|
||||
}
|
||||
|
||||
if (joinRuleChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
setBusy(false);
|
||||
const failures = results.filter(r => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error("Failed to save space settings: ", failures);
|
||||
setError(_t("Failed to save space settings."));
|
||||
}
|
||||
};
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
new Tab(
|
||||
SpaceSettingsTab.General,
|
||||
_td("General"),
|
||||
"mx_SpaceSettingsDialog_generalIcon",
|
||||
<SpaceSettingsGeneralTab matrixClient={cli} space={space} onFinished={onFinished} />,
|
||||
),
|
||||
new Tab(
|
||||
SpaceSettingsTab.Visibility,
|
||||
_td("Visibility"),
|
||||
"mx_SpaceSettingsDialog_visibilityIcon",
|
||||
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} />,
|
||||
),
|
||||
SettingsStore.getValue(UIFeature.AdvancedSettings)
|
||||
? new Tab(
|
||||
SpaceSettingsTab.Advanced,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
|
||||
)
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
}, [cli, space, onFinished]);
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Space settings")}
|
||||
|
@ -110,61 +80,14 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
|
||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
|
||||
<SpaceBasicSettings
|
||||
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
||||
avatarDisabled={busy || !canSetAvatar}
|
||||
setAvatar={setNewAvatar}
|
||||
name={name}
|
||||
nameDisabled={busy || !canSetName}
|
||||
setName={setName}
|
||||
topic={topic}
|
||||
topicDisabled={busy || !canSetTopic}
|
||||
setTopic={setTopic}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{ _t("Make this space private") }
|
||||
<ToggleSwitch
|
||||
checked={joinRule !== "public"}
|
||||
onChange={checked => setJoinRule(checked ? "invite" : "public")}
|
||||
disabled={!canSetJoinRule}
|
||||
aria-label={_t("Make this space private")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("Leave Space") }
|
||||
</AccessibleButton>
|
||||
|
||||
<div className="mx_SpaceSettingsDialog_buttons">
|
||||
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
|
||||
{ _t("View dev tools") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
||||
{ busy ? _t("Saving...") : _t("Save Changes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div
|
||||
className="mx_SpaceSettingsDialog_content"
|
||||
id="mx_SpaceSettingsDialog"
|
||||
title={_t("Settings - %(spaceName)s", { spaceName: space.name })}
|
||||
>
|
||||
<TabbedView tabs={tabs} />
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsDialog;
|
||||
|
||||
|
|
|
@ -16,22 +16,21 @@ limitations under the License.
|
|||
|
||||
import url from 'url';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t, pickBestLanguage } from '../../../languageHandler';
|
||||
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
||||
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||
|
||||
class TermsCheckbox extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool.isRequired,
|
||||
}
|
||||
interface ITermsCheckboxProps {
|
||||
onChange: (url: string, checked: boolean) => void;
|
||||
url: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.props.onChange(this.props.url, ev.target.checked);
|
||||
class TermsCheckbox extends React.PureComponent<ITermsCheckboxProps> {
|
||||
private onChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.props.onChange(this.props.url, ev.currentTarget.checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
interface ITermsDialogProps {
|
||||
/**
|
||||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: any[],
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls?: string[],
|
||||
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: (success: boolean, agreedUrls?: string[]) => void,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
agreedUrls: any;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.TermsDialog")
|
||||
export default class TermsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
/**
|
||||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: PropTypes.array.isRequired,
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default class TermsDialog extends React.PureComponent<ITermsDialogProps, IState> {
|
||||
constructor(props) {
|
||||
super();
|
||||
super(props);
|
||||
this.state = {
|
||||
// url -> boolean
|
||||
agreedUrls: {},
|
||||
|
@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
private onCancelClick = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onNextClick = () => {
|
||||
private onNextClick = (): void => {
|
||||
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
|
||||
}
|
||||
|
||||
_nameForServiceType(serviceType, host) {
|
||||
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
|
||||
switch (serviceType) {
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
||||
|
@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_summaryForServiceType(serviceType) {
|
||||
private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element {
|
||||
switch (serviceType) {
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>
|
||||
|
@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onTermsCheckboxChange = (url, checked) => {
|
||||
private onTermsCheckboxChange = (url: string, checked: boolean) => {
|
||||
this.setState({
|
||||
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
|
@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent {
|
|||
let serviceName;
|
||||
let summary;
|
||||
if (i === 0) {
|
||||
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
||||
summary = this._summaryForServiceType(
|
||||
serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
||||
summary = this.summaryForServiceType(
|
||||
policiesAndService.service.serviceType,
|
||||
);
|
||||
}
|
||||
|
@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent {
|
|||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a></td>
|
||||
<td>
|
||||
{termDoc[termsLang].name}
|
||||
<a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a>
|
||||
</td>
|
||||
<td><TermsCheckbox
|
||||
url={termDoc[termsLang].url}
|
||||
onChange={this._onTermsCheckboxChange}
|
||||
onChange={this.onTermsCheckboxChange}
|
||||
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
||||
/></td>
|
||||
</tr>);
|
||||
|
@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent {
|
|||
return (
|
||||
<BaseDialog
|
||||
fixedWidth={false}
|
||||
onFinished={this._onCancelClick}
|
||||
onFinished={this.onCancelClick}
|
||||
title={_t("Terms of Service")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
|
@ -197,8 +203,8 @@ export default class TermsDialog extends React.PureComponent {
|
|||
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancelClick}
|
||||
onPrimaryButtonClick={this._onNextClick}
|
||||
onCancel={this.onCancelClick}
|
||||
onPrimaryButtonClick={this.onNextClick}
|
||||
primaryDisabled={!enableSubmit}
|
||||
/>
|
||||
</BaseDialog>
|
|
@ -16,11 +16,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TabbedView, {Tab} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore";
|
||||
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
|
||||
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
|
||||
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
||||
|
@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
|
|||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
|
||||
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
|
||||
export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
|
||||
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
|
||||
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
|
||||
export const USER_VOICE_TAB = "USER_VOICE_TAB";
|
||||
export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
|
||||
export const USER_LABS_TAB = "USER_LABS_TAB";
|
||||
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
|
||||
export const USER_HELP_TAB = "USER_HELP_TAB";
|
||||
export enum UserTab {
|
||||
General = "USER_GENERAL_TAB",
|
||||
Appearance = "USER_APPEARANCE_TAB",
|
||||
Flair = "USER_FLAIR_TAB",
|
||||
Notifications = "USER_NOTIFICATIONS_TAB",
|
||||
Preferences = "USER_PREFERENCES_TAB",
|
||||
Voice = "USER_VOICE_TAB",
|
||||
Security = "USER_SECURITY_TAB",
|
||||
Labs = "USER_LABS_TAB",
|
||||
Mjolnir = "USER_MJOLNIR_TAB",
|
||||
Help = "USER_HELP_TAB",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
initialTabId?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mjolnirEnabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.UserSettingsDialog")
|
||||
export default class UserSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
initialTabId: PropTypes.string,
|
||||
};
|
||||
export default class UserSettingsDialog extends React.Component<IProps, IState> {
|
||||
private mjolnirWatcher: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
|
||||
public componentDidMount(): void {
|
||||
this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
SettingsStore.unwatchSetting(this._mjolnirWatcher);
|
||||
public componentWillUnmount(): void {
|
||||
SettingsStore.unwatchSetting(this.mjolnirWatcher);
|
||||
}
|
||||
|
||||
_mjolnirChanged(settingName, roomId, atLevel, newValue) {
|
||||
private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
|
||||
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
|
||||
this.setState({mjolnirEnabled: newValue});
|
||||
}
|
||||
|
@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component {
|
|||
const tabs = [];
|
||||
|
||||
tabs.push(new Tab(
|
||||
USER_GENERAL_TAB,
|
||||
UserTab.General,
|
||||
_td("General"),
|
||||
"mx_UserSettingsDialog_settingsIcon",
|
||||
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
USER_APPEARANCE_TAB,
|
||||
UserTab.Appearance,
|
||||
_td("Appearance"),
|
||||
"mx_UserSettingsDialog_appearanceIcon",
|
||||
<AppearanceUserSettingsTab />,
|
||||
));
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
tabs.push(new Tab(
|
||||
USER_FLAIR_TAB,
|
||||
UserTab.Flair,
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairUserSettingsTab />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
USER_NOTIFICATIONS_TAB,
|
||||
UserTab.Notifications,
|
||||
_td("Notifications"),
|
||||
"mx_UserSettingsDialog_bellIcon",
|
||||
<NotificationUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
USER_PREFERENCES_TAB,
|
||||
UserTab.Preferences,
|
||||
_td("Preferences"),
|
||||
"mx_UserSettingsDialog_preferencesIcon",
|
||||
<PreferencesUserSettingsTab />,
|
||||
|
@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component {
|
|||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
tabs.push(new Tab(
|
||||
USER_VOICE_TAB,
|
||||
UserTab.Voice,
|
||||
_td("Voice & Video"),
|
||||
"mx_UserSettingsDialog_voiceIcon",
|
||||
<VoiceUserSettingsTab />,
|
||||
|
@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component {
|
|||
}
|
||||
|
||||
tabs.push(new Tab(
|
||||
USER_SECURITY_TAB,
|
||||
UserTab.Security,
|
||||
_td("Security & Privacy"),
|
||||
"mx_UserSettingsDialog_securityIcon",
|
||||
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
|
@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component {
|
|||
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
|
||||
) {
|
||||
tabs.push(new Tab(
|
||||
USER_LABS_TAB,
|
||||
UserTab.Labs,
|
||||
_td("Labs"),
|
||||
"mx_UserSettingsDialog_labsIcon",
|
||||
<LabsUserSettingsTab />,
|
||||
|
@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component {
|
|||
}
|
||||
if (this.state.mjolnirEnabled) {
|
||||
tabs.push(new Tab(
|
||||
USER_MJOLNIR_TAB,
|
||||
UserTab.Mjolnir,
|
||||
_td("Ignored users"),
|
||||
"mx_UserSettingsDialog_mjolnirIcon",
|
||||
<MjolnirUserSettingsTab />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
USER_HELP_TAB,
|
||||
UserTab.Help,
|
||||
_td("Help & About"),
|
||||
"mx_UserSettingsDialog_helpIcon",
|
||||
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
<HelpUserSettingsTab closeSettingsFn={() => this.props.onFinished(true)} />,
|
||||
));
|
||||
|
||||
return tabs;
|
|
@ -15,22 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import * as sdk from "../../../../index";
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
|
||||
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onConfirm = () => {
|
||||
export default class ConfirmDestroyCrossSigningDialog extends React.Component<IProps> {
|
||||
private onConfirm = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onDecline = () => {
|
||||
private onDecline = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
|
@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Clear cross-signing keys")}
|
||||
onPrimaryButtonClick={this._onConfirm}
|
||||
onPrimaryButtonClick={this.onConfirm}
|
||||
primaryButtonClass="danger"
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onDecline}
|
||||
onCancel={this.onDecline}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import Modal from '../../../../Modal';
|
||||
|
@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons';
|
|||
import BaseDialog from '../BaseDialog';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
accountPassword?: string;
|
||||
tokenLogin?: boolean;
|
||||
onFinished?: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error: Error | null;
|
||||
canUploadKeysWithPasswordOnly?: boolean;
|
||||
accountPassword: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating a cross-signing keys. In most
|
||||
|
@ -33,39 +44,32 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
|||
* may need to complete some steps to proceed.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
|
||||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
export default class CreateCrossSigningDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
// Does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: null,
|
||||
accountPassword: props.accountPassword || "",
|
||||
};
|
||||
|
||||
if (this.state.accountPassword) {
|
||||
// If we have an account password in memory, let's simplify and
|
||||
// assume it means password auth is also supported for device
|
||||
// signing key upload as well. This avoids hitting the server to
|
||||
// test auth flows, which may be slow under high load.
|
||||
this.state.canUploadKeysWithPasswordOnly = true;
|
||||
} else {
|
||||
this._queryKeyUploadAuth();
|
||||
canUploadKeysWithPasswordOnly: props.accountPassword ? true : null,
|
||||
accountPassword: props.accountPassword || "",
|
||||
};
|
||||
|
||||
if (!this.state.accountPassword) {
|
||||
this.queryKeyUploadAuth();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._bootstrapCrossSigning();
|
||||
public componentDidMount(): void {
|
||||
this.bootstrapCrossSigning();
|
||||
}
|
||||
|
||||
async _queryKeyUploadAuth() {
|
||||
private async queryKeyUploadAuth(): Promise<void> {
|
||||
try {
|
||||
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
||||
// We should never get here: the server should always require
|
||||
|
@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_doBootstrapUIAuth = async (makeRequest) => {
|
||||
private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise<void> => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
type: 'm.login.password',
|
||||
|
@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_bootstrapCrossSigning = async () => {
|
||||
private bootstrapCrossSigning = async (): Promise<void> => {
|
||||
this.setState({
|
||||
error: null,
|
||||
});
|
||||
|
@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
|
||||
try {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
if (this.props.tokenLogin) {
|
||||
// ignore any failures, we are relying on grace period here
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
|
@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
<p>{_t("Unable to set up keys")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapCrossSigning}
|
||||
onCancel={this._onCancel}
|
||||
onPrimaryButtonClick={this.bootstrapCrossSigning}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
|
@ -15,47 +15,52 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
|
||||
import BaseDialog from '../BaseDialog';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
|
||||
import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
|
||||
function iconFromPhase(phase) {
|
||||
if (phase === PHASE_DONE) {
|
||||
function iconFromPhase(phase: Phase) {
|
||||
if (phase === Phase.Done) {
|
||||
return require("../../../../../res/img/e2e/verified.svg");
|
||||
} else {
|
||||
return require("../../../../../res/img/e2e/warning.svg");
|
||||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
|
||||
export default class SetupEncryptionDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IState {
|
||||
icon: Phase;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
|
||||
export default class SetupEncryptionDialog extends React.Component<IProps, IState> {
|
||||
private store: SetupEncryptionStore;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.store = SetupEncryptionStore.sharedInstance();
|
||||
this.state = {icon: iconFromPhase(this.store.phase)};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.store.on("update", this._onStoreUpdate);
|
||||
public componentDidMount() {
|
||||
this.store.on("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.store.removeListener("update", this._onStoreUpdate);
|
||||
public componentWillUnmount() {
|
||||
this.store.removeListener("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
_onStoreUpdate = () => {
|
||||
private onStoreUpdate = (): void => {
|
||||
this.setState({icon: iconFromPhase(this.store.phase)});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
return <BaseDialog
|
||||
headerImage={this.state.icon}
|
||||
onFinished={this.props.onFinished}
|
|
@ -62,6 +62,8 @@ export default function AccessibleButton({
|
|||
disabled,
|
||||
inputRef,
|
||||
className,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
...restProps
|
||||
}: IProps) {
|
||||
const newProps: IAccessibleButtonProps = restProps;
|
||||
|
@ -83,6 +85,8 @@ export default function AccessibleButton({
|
|||
if (e.key === Key.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
};
|
||||
newProps.onKeyUp = (e) => {
|
||||
|
@ -94,6 +98,8 @@ export default function AccessibleButton({
|
|||
if (e.key === Key.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onKeyUp?.(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
import TagTile from './TagTile';
|
||||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
|
@ -31,32 +30,17 @@ export default function DNDTagTile(props) {
|
|||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
|
||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} />
|
||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} index={props.index} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
return <div>
|
||||
<Draggable
|
||||
key={props.tag}
|
||||
draggableId={props.tag}
|
||||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile
|
||||
{...props}
|
||||
contextMenuButtonRef={handle}
|
||||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
return <>
|
||||
<TagTile
|
||||
{...props}
|
||||
contextMenuButtonRef={handle}
|
||||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
{contextMenu}
|
||||
</div>;
|
||||
</>;
|
||||
}
|
||||
|
|
|
@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import EventIndexPeg from "../../../indexing/EventIndexPeg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import React from "react";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||
|
||||
|
||||
export enum WarningKind {
|
||||
Files,
|
||||
|
@ -33,6 +37,22 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
|
|||
if (!isRoomEncrypted) return null;
|
||||
if (EventIndexPeg.get()) return null;
|
||||
|
||||
if (EventIndexPeg.error) {
|
||||
return <>
|
||||
{_t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
|
||||
a: sub => (<a onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}}>
|
||||
{sub}
|
||||
</a>),
|
||||
})}
|
||||
</>;
|
||||
}
|
||||
|
||||
const {desktopBuilds, brand} = SdkConfig.get();
|
||||
|
||||
let text = null;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017, 2019 New Vector Ltd.
|
||||
Copyright 2017-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.
|
||||
|
@ -14,48 +14,48 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "./Field";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export class EditableItem extends React.Component {
|
||||
static propTypes = {
|
||||
index: PropTypes.number,
|
||||
value: PropTypes.string,
|
||||
onRemove: PropTypes.func,
|
||||
interface IItemProps {
|
||||
index?: number;
|
||||
value?: string;
|
||||
onRemove?(index: number): void;
|
||||
}
|
||||
|
||||
interface IItemState {
|
||||
verifyRemove: boolean;
|
||||
}
|
||||
|
||||
export class EditableItem extends React.Component<IItemProps, IItemState> {
|
||||
public state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: true});
|
||||
this.setState({ verifyRemove: true });
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
private onDontRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: false});
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
private onActuallyRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||
this.setState({verifyRemove: false});
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
|
|||
{_t("Are you sure?")}
|
||||
</span>
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
onClick={this.onActuallyRemove}
|
||||
kind="primary_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
{_t("Yes")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
onClick={this.onDontRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
|
@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
id: string;
|
||||
items: string[];
|
||||
itemsLabel?: string;
|
||||
noItemsLabel?: string;
|
||||
placeholder?: string;
|
||||
newItem?: string;
|
||||
canEdit?: boolean;
|
||||
canRemove?: boolean;
|
||||
suggestionsListId?: string;
|
||||
onItemAdded?(item: string): void;
|
||||
onItemRemoved?(index: number): void;
|
||||
onNewItemChanged?(item: string): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.EditableItemList")
|
||||
export default class EditableItemList extends React.Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
itemsLabel: PropTypes.string,
|
||||
noItemsLabel: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
newItem: PropTypes.string,
|
||||
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
canRemove: PropTypes.bool,
|
||||
};
|
||||
|
||||
_onItemAdded = (e) => {
|
||||
export default class EditableItemList<P = {}> extends React.PureComponent<IProps & P> {
|
||||
protected onItemAdded = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
};
|
||||
|
||||
_onItemRemoved = (index) => {
|
||||
protected onItemRemoved = (index) => {
|
||||
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
|
||||
};
|
||||
|
||||
_onNewItemChanged = (e) => {
|
||||
protected onNewItemChanged = (e) => {
|
||||
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
protected renderNewItemField() {
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._onItemAdded}
|
||||
onSubmit={this.onItemAdded}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EditableItemList_newItem"
|
||||
>
|
||||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
||||
{_t("Add")}
|
||||
<Field
|
||||
label={this.props.placeholder}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={this.props.newItem || ""}
|
||||
onChange={this.onNewItemChanged}
|
||||
list={this.props.suggestionsListId}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onItemAdded}
|
||||
kind="primary"
|
||||
type="submit"
|
||||
disabled={!this.props.newItem}
|
||||
>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
|
@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
|
|||
key={item}
|
||||
index={index}
|
||||
value={item}
|
||||
onRemove={this._onItemRemoved}
|
||||
onRemove={this.onItemRemoved}
|
||||
/>;
|
||||
});
|
||||
|
||||
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
|
||||
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
|
||||
|
||||
return (<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
return (
|
||||
<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
</div>
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this.renderNewItemField() : <div /> }
|
||||
</div>
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
|
||||
</div>);
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
|
@ -101,7 +102,8 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
name: this.props.displayName,
|
||||
name: this.props.displayName || this.props.userId,
|
||||
rawDisplayName: this.props.displayName,
|
||||
userId: this.props.userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return Avatar.avatarUrlForUser(
|
||||
|
@ -110,7 +112,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
);
|
||||
},
|
||||
getMxcAvatarUrl: () => this.props.avatarUrl,
|
||||
};
|
||||
} as RoomMember;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@ function getId() {
|
|||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
export interface IValidateOpts {
|
||||
focused?: boolean;
|
||||
allowEmpty?: boolean;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id?: string;
|
||||
|
@ -180,7 +185,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
|
||||
public async validate({ focused, allowEmpty = true }: IValidateOpts) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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 AccessibleButton from "./AccessibleButton";
|
||||
|
||||
export default function FormButton(props) {
|
||||
const {className, label, kind, ...restProps} = props;
|
||||
const newClassName = (className || "") + " mx_FormButton";
|
||||
const allProps = Object.assign({}, restProps,
|
||||
{className: newClassName, kind: kind || "primary", children: [label]});
|
||||
return React.createElement(AccessibleButton, allProps);
|
||||
}
|
||||
|
||||
FormButton.propTypes = AccessibleButton.propTypes;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,38 +14,33 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
|
||||
import ToggleSwitch from "./ToggleSwitch";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// The value for the toggle switch
|
||||
value: boolean;
|
||||
// The translated label for the switch
|
||||
label: string;
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled?: boolean;
|
||||
// True to put the toggle in front of the label
|
||||
// Default false.
|
||||
toggleInFront?: boolean;
|
||||
// Additional class names to append to the switch. Optional.
|
||||
className?: string;
|
||||
// The function to call when the value changes
|
||||
onChange(checked: boolean): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.LabelledToggleSwitch")
|
||||
export default class LabelledToggleSwitch extends React.Component {
|
||||
static propTypes = {
|
||||
// The value for the toggle switch
|
||||
value: PropTypes.bool.isRequired,
|
||||
|
||||
// The function to call when the value changes
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
// The translated label for the switch
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
// True to put the toggle in front of the label
|
||||
// Default false.
|
||||
toggleInFront: PropTypes.bool,
|
||||
|
||||
// Additional class names to append to the switch. Optional.
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
// This is a minimal version of a SettingsFlag
|
||||
|
||||
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
|
||||
let firstPart = <span className="mx_SettingsFlag_label">{ this.props.label }</span>;
|
||||
let secondPart = <ToggleSwitch
|
||||
checked={this.props.value}
|
||||
disabled={this.props.disabled}
|
|
@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler';
|
|||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import EventListSummary from "./EventListSummary";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// An array of member events to summarise
|
||||
|
@ -303,7 +303,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
return res;
|
||||
}
|
||||
|
||||
private static getTransitionSequence(events: MatrixEvent[]) {
|
||||
private static getTransitionSequence(events: IUserEvents[]) {
|
||||
return events.map(MemberEventListSummary.getTransition);
|
||||
}
|
||||
|
||||
|
@ -315,7 +315,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* @returns {string?} the transition type given to this event. This defaults to `null`
|
||||
* if a transition is not recognised.
|
||||
*/
|
||||
private static getTransition(e: MatrixEvent): TransitionType {
|
||||
private static getTransition(e: IUserEvents): TransitionType {
|
||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||
// Handle 3pid invites the same as invites so they get bundled together
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
|
|
|
@ -297,6 +297,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
async getEvent(eventId) {
|
||||
if (!eventId) return null;
|
||||
const event = this.room.findEventById(eventId);
|
||||
if (event) return event;
|
||||
|
||||
|
@ -392,6 +393,7 @@ export default class ReplyThread extends React.Component {
|
|||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
replacingEventId={ev.replacingEventId()}
|
||||
as="div"
|
||||
/>
|
||||
</blockquote>;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -13,67 +13,78 @@ 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, { createRef } from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import withValidation from './Validation';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field, { IValidateOpts } from "./Field";
|
||||
|
||||
interface IProps {
|
||||
domain: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
onChange?(value: string): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
||||
@replaceableComponent("views.elements.RoomAliasField")
|
||||
export default class RoomAliasField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class RoomAliasField extends React.PureComponent<IProps, IState> {
|
||||
private fieldRef = createRef<Field>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isValid: true};
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
_asFullAlias(localpart) {
|
||||
private asFullAlias(localpart: string): string {
|
||||
return `#${localpart}:${this.props.domain}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
const poundSign = (<span>#</span>);
|
||||
const aliasPostfix = ":" + this.props.domain;
|
||||
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
label={_t("Room address")}
|
||||
label={this.props.label || _t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefixComponent={poundSign}
|
||||
postfixComponent={domain}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
onChange={this._onChange}
|
||||
ref={this.fieldRef}
|
||||
onValidate={this.onValidate}
|
||||
placeholder={this.props.placeholder || _t("e.g. my-room")}
|
||||
onChange={this.onChange}
|
||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onChange = (ev) => {
|
||||
private onChange = (ev) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this._asFullAlias(ev.target.value));
|
||||
this.props.onChange(this.asFullAlias(ev.target.value));
|
||||
}
|
||||
};
|
||||
|
||||
_onValidate = async (fieldState) => {
|
||||
const result = await this._validationRules(fieldState);
|
||||
private onValidate = async (fieldState) => {
|
||||
const result = await this.validationRules(fieldState);
|
||||
this.setState({isValid: result.valid});
|
||||
return result;
|
||||
};
|
||||
|
||||
_validationRules = withValidation({
|
||||
private validationRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "safeLocalpart",
|
||||
|
@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
const fullAlias = this._asFullAlias(value);
|
||||
const fullAlias = this.asFullAlias(value);
|
||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
|
||||
encodeURI(fullAlias) === fullAlias;
|
||||
|
@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}, {
|
||||
key: "required",
|
||||
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Please provide a room address"),
|
||||
invalid: () => _t("Please provide an address"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
|
@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}
|
||||
const client = MatrixClientPeg.get();
|
||||
try {
|
||||
await client.getRoomIdForAlias(this._asFullAlias(value));
|
||||
await client.getRoomIdForAlias(this.asFullAlias(value));
|
||||
// we got a room id, so the alias is taken
|
||||
return false;
|
||||
} catch (err) {
|
||||
|
@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
],
|
||||
});
|
||||
|
||||
get isValid() {
|
||||
public get isValid() {
|
||||
return this.state.isValid;
|
||||
}
|
||||
|
||||
validate(options) {
|
||||
return this._fieldRef.validate(options);
|
||||
public validate(options: IValidateOpts) {
|
||||
return this.fieldRef.current?.validate(options);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._fieldRef.focus();
|
||||
public focus() {
|
||||
this.fieldRef.current?.focus();
|
||||
}
|
||||
}
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => {
|
|||
}, [room]);
|
||||
|
||||
if (children) return children(name);
|
||||
return name || "";
|
||||
return <>{ name || "" }</>;
|
||||
};
|
||||
|
||||
export default RoomName;
|
||||
|
|
|
@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
public render() {
|
||||
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
|
||||
|
||||
let label = this.props.label;
|
||||
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
else label = _t(label);
|
||||
const label = this.props.label
|
||||
? _t(this.props.label)
|
||||
: SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
const description = SettingsStore.getDescription(this.props.name);
|
||||
|
||||
if (this.props.useCheckbox) {
|
||||
return <StyledCheckbox
|
||||
|
@ -99,6 +100,9 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
/>
|
||||
{ description && <div className="mx_SettingsFlag_microcopy">
|
||||
{ description }
|
||||
</div> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,10 +34,19 @@ interface IProps<T extends string> {
|
|||
definitions: IDefinition<T>[];
|
||||
value?: T; // if not provided no options will be selected
|
||||
outlined?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange(newValue: T): void;
|
||||
}
|
||||
|
||||
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
|
||||
function StyledRadioGroup<T extends string>({
|
||||
name,
|
||||
definitions,
|
||||
value,
|
||||
className,
|
||||
outlined,
|
||||
disabled,
|
||||
onChange,
|
||||
}: IProps<T>) {
|
||||
const _onChange = e => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
@ -50,12 +59,12 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
|
|||
checked={d.checked !== undefined ? d.checked : d.value === value}
|
||||
name={name}
|
||||
value={d.value}
|
||||
disabled={d.disabled}
|
||||
disabled={disabled || d.disabled}
|
||||
outlined={outlined}
|
||||
>
|
||||
{d.label}
|
||||
{ d.label }
|
||||
</StyledRadioButton>
|
||||
{d.description}
|
||||
{ d.description ? <span>{ d.description }</span> : null }
|
||||
</React.Fragment>)}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
|
|
@ -16,31 +16,29 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.elements.TruncatedList")
|
||||
export default class TruncatedList extends React.Component {
|
||||
static propTypes = {
|
||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||
truncateAt: PropTypes.number,
|
||||
// The className to apply to the wrapping div
|
||||
className: PropTypes.string,
|
||||
// A function that returns the children to be rendered into the element.
|
||||
// function getChildren(start: number, end: number): Array<React.Node>
|
||||
// The start element is included, the end is not (as in `slice`).
|
||||
// If omitted, the React child elements will be used. This parameter can be used
|
||||
// to avoid creating unnecessary React elements.
|
||||
getChildren: PropTypes.func,
|
||||
// A function that should return the total number of child element available.
|
||||
// Required if getChildren is supplied.
|
||||
getChildCount: PropTypes.func,
|
||||
// A function which will be invoked when an overflow element is required.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement: PropTypes.func,
|
||||
};
|
||||
interface IProps {
|
||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||
truncateAt?: number;
|
||||
// The className to apply to the wrapping div
|
||||
className?: string;
|
||||
// A function that returns the children to be rendered into the element.
|
||||
// The start element is included, the end is not (as in `slice`).
|
||||
// If omitted, the React child elements will be used. This parameter can be used
|
||||
// to avoid creating unnecessary React elements.
|
||||
getChildren?: (start: number, end: number) => Array<React.ReactNode>;
|
||||
// A function that should return the total number of child element available.
|
||||
// Required if getChildren is supplied.
|
||||
getChildCount?: () => number;
|
||||
// A function which will be invoked when an overflow element is required.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.TruncatedList")
|
||||
export default class TruncatedList extends React.Component<IProps> {
|
||||
static defaultProps ={
|
||||
truncateAt: 2,
|
||||
createOverflowElement(overflowCount, totalCount) {
|
||||
|
@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
|
|||
},
|
||||
};
|
||||
|
||||
_getChildren(start, end) {
|
||||
private getChildren(start: number, end: number): Array<React.ReactNode> {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildren(start, end);
|
||||
} else {
|
||||
|
@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getChildCount() {
|
||||
private getChildCount(): number {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildCount();
|
||||
} else {
|
||||
|
@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
let overflowNode = null;
|
||||
|
||||
const totalChildren = this._getChildCount();
|
||||
const totalChildren = this.getChildCount();
|
||||
let upperBound = totalChildren;
|
||||
if (this.props.truncateAt >= 0) {
|
||||
const overflowCount = totalChildren - this.props.truncateAt;
|
||||
|
@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
|
|||
upperBound = this.props.truncateAt;
|
||||
}
|
||||
}
|
||||
const childNodes = this._getChildren(0, upperBound);
|
||||
const childNodes = this.getChildren(0, upperBound);
|
||||
|
||||
return (
|
||||
<div className={this.props.className}>
|
|
@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component {
|
|||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false}
|
||||
avatarHeight={40} draggable={false}
|
||||
/>
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||
<ToggleSwitch checked={this.state.isGroupPublicised}
|
||||
disabled={!this.state.ready || this.state.busy}
|
||||
onChange={this._onPublicityToggle} />
|
||||
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
function nop() {}
|
||||
import { _t } from "../../../languageHandler";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
|
||||
@replaceableComponent("views.groups.GroupTile")
|
||||
class GroupTile extends React.Component {
|
||||
|
@ -34,7 +34,6 @@ class GroupTile extends React.Component {
|
|||
showDescription: PropTypes.bool,
|
||||
// Height of the group avatar in pixels
|
||||
avatarHeight: PropTypes.number,
|
||||
draggable: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -42,7 +41,6 @@ class GroupTile extends React.Component {
|
|||
static defaultProps = {
|
||||
showDescription: true,
|
||||
avatarHeight: 50,
|
||||
draggable: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -57,7 +55,7 @@ class GroupTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onMouseDown = e => {
|
||||
onClick = e => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
|
@ -65,6 +63,18 @@ class GroupTile extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onPinClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
|
||||
};
|
||||
|
||||
onUnpinClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
@ -78,7 +88,7 @@ class GroupTile extends React.Component {
|
|||
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
|
||||
: null;
|
||||
|
||||
let avatarElement = (
|
||||
const avatarElement = (
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
|
@ -88,46 +98,21 @@ class GroupTile extends React.Component {
|
|||
height={avatarHeight} />
|
||||
</div>
|
||||
);
|
||||
if (this.props.draggable) {
|
||||
const avatarClone = avatarElement;
|
||||
avatarElement = (
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
<Draggable
|
||||
key={"GroupTile " + this.props.groupId}
|
||||
draggableId={"GroupTile " + this.props.groupId}
|
||||
index={this.props.groupId}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{avatarClone}
|
||||
</div>
|
||||
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
||||
{ provided.placeholder ? avatarClone : <div /> }
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
);
|
||||
}
|
||||
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||
{ avatarElement }
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
|
||||
{ !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
|
||||
? <AccessibleButton kind="link" onClick={this.onPinClick}>
|
||||
{ _t("Pin") }
|
||||
</AccessibleButton>
|
||||
: <AccessibleButton kind="link" onClick={this.onUnpinClick}>
|
||||
{ _t("Unpin") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
|
|
@ -15,41 +15,40 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {getNameForEventRoom, userLabelForEventRoom}
|
||||
import { getNameForEventRoom, userLabelForEventRoom }
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationRequest")
|
||||
export default class MKeyVerificationRequest extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
export default class MKeyVerificationRequest extends React.Component<IProps> {
|
||||
public componentDidMount() {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.on("change", this._onRequestChanged);
|
||||
request.on("change", this.onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount() {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.off("change", this._onRequestChanged);
|
||||
request.off("change", this.onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
_openRequest = () => {
|
||||
const {verificationRequest} = this.props.mxEvent;
|
||||
private openRequest = () => {
|
||||
const { verificationRequest } = this.props.mxEvent;
|
||||
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
|
@ -58,15 +57,15 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onRequestChanged = () => {
|
||||
private onRequestChanged = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_onAcceptClicked = async () => {
|
||||
private onAcceptClicked = async () => {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
this._openRequest();
|
||||
this.openRequest();
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
|
@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onRejectClicked = async () => {
|
||||
private onRejectClicked = async () => {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
|
@ -85,7 +84,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_acceptedLabel(userId) {
|
||||
private acceptedLabel(userId: string) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const myUserId = client.getUserId();
|
||||
if (userId === myUserId) {
|
||||
|
@ -95,7 +94,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_cancelledLabel(userId) {
|
||||
private cancelledLabel(userId: string) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const myUserId = client.getUserId();
|
||||
const {cancellationCode} = this.props.mxEvent.verificationRequest;
|
||||
|
@ -115,9 +114,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
|
||||
const {mxEvent} = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
|
@ -134,11 +132,11 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
let stateLabel;
|
||||
const accepted = request.ready || request.started || request.done;
|
||||
if (accepted) {
|
||||
stateLabel = (<AccessibleButton onClick={this._openRequest}>
|
||||
{this._acceptedLabel(request.receivingUserId)}
|
||||
stateLabel = (<AccessibleButton onClick={this.openRequest}>
|
||||
{this.acceptedLabel(request.receivingUserId)}
|
||||
</AccessibleButton>);
|
||||
} else if (request.cancelled) {
|
||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||
stateLabel = this.cancelledLabel(request.cancellingUserId);
|
||||
} else if (request.accepting) {
|
||||
stateLabel = _t("Accepting …");
|
||||
} else if (request.declining) {
|
||||
|
@ -153,8 +151,12 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||
if (request.canAccept) {
|
||||
stateNode = (<div className="mx_cryptoEvent_buttons">
|
||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
|
||||
<AccessibleButton kind="danger" onClick={this.onRejectClicked}>
|
||||
{_t("Decline")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onAcceptClicked}>
|
||||
{_t("Accept")}
|
||||
</AccessibleButton>
|
||||
</div>);
|
||||
}
|
||||
} else { // request sent by us
|
||||
|
@ -174,8 +176,3 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MKeyVerificationRequest.propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
|
@ -24,6 +24,7 @@ import {_t} from "../../../languageHandler";
|
|||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
import {decryptFile} from "../../../utils/DecryptFile";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -45,7 +46,7 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
|
|||
|
||||
public async componentDidMount() {
|
||||
let buffer: ArrayBuffer;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const content: IMediaEventContent = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
try {
|
||||
|
|
|
@ -28,7 +28,9 @@ interface IProps {
|
|||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||
export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
|
||||
public render() {
|
||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
|
||||
// MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
|
||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
|
||||
|| !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
|
||||
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
||||
if (isVoiceMessage && voiceMessagesEnabled) {
|
||||
return <MVoiceMessageBody {...this.props} />;
|
||||
|
|
|
@ -16,20 +16,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
ts: number;
|
||||
showTwelveHour?: boolean;
|
||||
showFullDate?: boolean;
|
||||
showSeconds?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MessageTimestamp")
|
||||
export default class MessageTimestamp extends React.Component {
|
||||
static propTypes = {
|
||||
ts: PropTypes.number.isRequired,
|
||||
showTwelveHour: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
showSeconds: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
export default class MessageTimestamp extends React.Component<IProps> {
|
||||
public render() {
|
||||
const date = new Date(this.props.ts);
|
||||
let timestamp;
|
||||
if (this.props.showFullDate) {
|
||||
|
@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
|
||||
<span
|
||||
className="mx_MessageTimestamp"
|
||||
title={formatFullDate(date, this.props.showTwelveHour)}
|
||||
aria-hidden={true}
|
||||
>
|
||||
{timestamp}
|
||||
</span>
|
||||
);
|
|
@ -17,10 +17,10 @@
|
|||
import React from 'react';
|
||||
import Flair from '../elements/Flair';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
|
|
@ -16,12 +16,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import highlight from 'highlight.js';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import { formatDate } from '../../../DateUtils';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -29,14 +29,16 @@ import { _t } from '../../../languageHandler';
|
|||
import * as ContextMenu from '../../structures/ContextMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import { isPermalinkHost } from "../../../utils/permalinks/Permalinks";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
@replaceableComponent("views.messages.TextualBody")
|
||||
export default class TextualBody extends React.Component {
|
||||
|
@ -390,9 +392,9 @@ export default class TextualBody extends React.Component {
|
|||
|
||||
onEmoteSenderClick = event => {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
dis.dispatch({
|
||||
action: 'insert_mention',
|
||||
user_id: mxEvent.getSender(),
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: mxEvent.getSender(),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -17,8 +17,9 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
export const PendingActionSpinner = ({text}) => {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
@ -31,7 +32,7 @@ export const PendingActionSpinner = ({text}) => {
|
|||
interface IProps {
|
||||
waitingForOtherParty: boolean;
|
||||
waitingForNetwork: boolean;
|
||||
member: RoomMember;
|
||||
member: RoomMember | User;
|
||||
onStartVerification: () => Promise<void>;
|
||||
isRoomEncrypted: boolean;
|
||||
inDialog: boolean;
|
||||
|
@ -55,7 +56,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
text = _t("Accept on your other login…");
|
||||
} else {
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -14,28 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { PHASE_REQUESTED, PHASE_UNSENT } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import EncryptionInfo from "./EncryptionInfo";
|
||||
import VerificationPanel from "./VerificationPanel";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {ensureDMExists} from "../../../createRoom";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import Modal from "../../../Modal";
|
||||
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
|
||||
// cancellation codes which constitute a key mismatch
|
||||
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
||||
|
||||
interface IProps {
|
||||
member: RoomMember;
|
||||
member: RoomMember | User;
|
||||
onClose: () => void;
|
||||
verificationRequest: VerificationRequest;
|
||||
verificationRequestPromise: Promise<VerificationRequest>;
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React, {useCallback, useContext, useEffect, useState} from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
|
@ -28,6 +27,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
|||
import PinningUtils from "../../../utils/PinningUtils";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import PinnedEventTile from "../rooms/PinnedEventTile";
|
||||
import { useRoomState } from "../../../hooks/useRoomState";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -75,24 +75,6 @@ export const useReadPinnedEvents = (room: Room): Set<string> => {
|
|||
return readPinnedEvents;
|
||||
};
|
||||
|
||||
const useRoomState = <T extends any>(room: Room, mapper: (state: RoomState) => T): T => {
|
||||
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (!room) return;
|
||||
setValue(mapper(room.currentState));
|
||||
}, [room, mapper]);
|
||||
|
||||
useEventEmitter(room?.currentState, "RoomState.events", update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
setValue(undefined);
|
||||
};
|
||||
}, [update]);
|
||||
return value;
|
||||
};
|
||||
|
||||
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
||||
|
|
|
@ -38,7 +38,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { textualPowerLevel } from '../../../Roles';
|
||||
|
@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel";
|
|||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
||||
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
@ -68,6 +68,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
|||
import RoomName from "../elements/RoomName";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -146,7 +147,7 @@ async function openDMForUser(matrixClient: MatrixClient, userId: string) {
|
|||
|
||||
type SetUpdating = (updating: boolean) => void;
|
||||
|
||||
function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) {
|
||||
function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean, setUpdating: SetUpdating) {
|
||||
return useAsyncMemo(async () => {
|
||||
if (!canVerify) {
|
||||
return undefined;
|
||||
|
@ -368,9 +369,9 @@ const UserOptionsSection: React.FC<{
|
|||
};
|
||||
|
||||
const onInsertPillButton = function() {
|
||||
dis.dispatch({
|
||||
action: 'insert_mention',
|
||||
user_id: member.userId,
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: member.userId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -502,19 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
|
|||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||
|
||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||
|
||||
const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (event) {
|
||||
setPowerLevels(event.getContent());
|
||||
} else {
|
||||
setPowerLevels({});
|
||||
}
|
||||
setPowerLevels(getPowerLevels(room));
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(cli, "RoomState.events", update);
|
||||
|
@ -971,7 +968,7 @@ interface IRoomPermissions {
|
|||
canInvite: boolean;
|
||||
}
|
||||
|
||||
function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions {
|
||||
function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions {
|
||||
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
modifyLevelMax: -1,
|
||||
|
@ -1028,7 +1025,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPer
|
|||
}
|
||||
|
||||
const PowerLevelSection: React.FC<{
|
||||
user: User;
|
||||
user: RoomMember;
|
||||
room: Room;
|
||||
roomPermissions: IRoomPermissions;
|
||||
powerLevels: IPowerLevelsContent;
|
||||
|
@ -1037,7 +1034,7 @@ const PowerLevelSection: React.FC<{
|
|||
return (<PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />);
|
||||
} else {
|
||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||
const powerLevel = parseInt(user.powerLevel, 10);
|
||||
const powerLevel = user.powerLevel;
|
||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
|
@ -1048,13 +1045,13 @@ const PowerLevelSection: React.FC<{
|
|||
};
|
||||
|
||||
const PowerLevelEditor: React.FC<{
|
||||
user: User;
|
||||
user: RoomMember;
|
||||
room: Room;
|
||||
roomPermissions: IRoomPermissions;
|
||||
}> = ({user, room, roomPermissions}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
||||
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
||||
const powerLevel = parseInt(powerLevelStr, 10);
|
||||
setSelectedPowerLevel(powerLevel);
|
||||
|
@ -1231,7 +1228,7 @@ const BasicUserInfo: React.FC<{
|
|||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, member);
|
||||
const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||
|
@ -1275,12 +1272,26 @@ const BasicUserInfo: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
let memberDetails;
|
||||
let adminToolsContainer;
|
||||
if (room && member.roomId) {
|
||||
if (room && (member as RoomMember).roomId) {
|
||||
// hide the Roles section for DMs as it doesn't make sense there
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
|
||||
memberDetails = <div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Role") }</h3>
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member as RoomMember}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
adminToolsContainer = (
|
||||
<RoomAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={member}
|
||||
member={member as RoomMember}
|
||||
room={room}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
|
@ -1309,20 +1320,6 @@ const BasicUserInfo: React.FC<{
|
|||
spinner = <Spinner />;
|
||||
}
|
||||
|
||||
let memberDetails;
|
||||
// hide the Roles section for DMs as it doesn't make sense there
|
||||
if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) {
|
||||
memberDetails = <div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Role") }</h3>
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
|
||||
|
@ -1349,8 +1346,7 @@ const BasicUserInfo: React.FC<{
|
|||
const setUpdating = (updating) => {
|
||||
setPendingUpdateCount(count => count + (updating ? 1 : -1));
|
||||
};
|
||||
const hasCrossSigningKeys =
|
||||
useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
|
||||
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
|
||||
|
||||
const showDeviceListSpinner = devices === undefined;
|
||||
if (canVerify) {
|
||||
|
@ -1359,9 +1355,9 @@ const BasicUserInfo: React.FC<{
|
|||
verifyButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {
|
||||
if (hasCrossSigningKeys) {
|
||||
verifyUser(member);
|
||||
verifyUser(member as User);
|
||||
} else {
|
||||
legacyVerifyUser(member);
|
||||
legacyVerifyUser(member as User);
|
||||
}
|
||||
}}>
|
||||
{_t("Verify")}
|
||||
|
@ -1381,7 +1377,7 @@ const BasicUserInfo: React.FC<{
|
|||
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: USER_SECURITY_TAB,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}}>
|
||||
{ _t("Edit devices") }
|
||||
|
@ -1409,7 +1405,7 @@ const BasicUserInfo: React.FC<{
|
|||
<UserOptionsSection
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member}
|
||||
member={member as RoomMember}
|
||||
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
|
||||
/>
|
||||
|
||||
|
@ -1428,13 +1424,15 @@ const UserInfoHeader: React.FC<{
|
|||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
||||
? (member as RoomMember).getMxcAvatarUrl()
|
||||
: (member as User).avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: member.name,
|
||||
name: (member as RoomMember).name || (member as User).displayName,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
|
@ -1446,13 +1444,13 @@ const UserInfoHeader: React.FC<{
|
|||
<div>
|
||||
<MemberAvatar
|
||||
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
||||
member={member}
|
||||
member={member as RoomMember}
|
||||
width={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||
height={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={onMemberAvatarClick}
|
||||
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
||||
urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1469,7 +1467,11 @@ const UserInfoHeader: React.FC<{
|
|||
presenceCurrentlyActive = member.user.currentlyActive;
|
||||
|
||||
if (SettingsStore.getValue("feature_custom_status")) {
|
||||
statusMessage = member.user._unstable_statusMessage;
|
||||
if ((member as RoomMember).user) {
|
||||
statusMessage = member.user.unstable_statusMessage;
|
||||
} else {
|
||||
statusMessage = (member as unknown as User).unstable_statusMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1500,7 +1502,7 @@ const UserInfoHeader: React.FC<{
|
|||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
|
||||
const displayName = member.rawDisplayName || member.displayname;
|
||||
const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname;
|
||||
return <React.Fragment>
|
||||
{ avatarElement }
|
||||
|
||||
|
@ -1588,7 +1590,7 @@ const UserInfo: React.FC<IProps> = ({
|
|||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
member={member}
|
||||
member={member as User}
|
||||
groupId={groupId as string}
|
||||
devices={devices}
|
||||
isRoomEncrypted={isRoomEncrypted} />
|
||||
|
@ -1599,7 +1601,7 @@ const UserInfo: React.FC<IProps> = ({
|
|||
content = (
|
||||
<EncryptionPanel
|
||||
{...props as React.ComponentProps<typeof EncryptionPanel>}
|
||||
member={member}
|
||||
member={member as User | RoomMember}
|
||||
onClose={onEncryptionPanelClose}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
|
|
|
@ -22,6 +22,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
|||
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
|
||||
|
@ -51,7 +52,7 @@ enum VerificationPhase {
|
|||
interface IProps {
|
||||
layout: string;
|
||||
request: VerificationRequest;
|
||||
member: RoomMember;
|
||||
member: RoomMember | User;
|
||||
phase: VerificationPhase;
|
||||
onClose: () => void;
|
||||
isRoomEncrypted: boolean;
|
||||
|
@ -134,7 +135,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
qrBlock = <div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify by scanning")}</h3>
|
||||
<p>{_t("Ask %(displayName)s to scan your code:", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
})}</p>
|
||||
|
||||
<div className="mx_VerificationPanel_qrCode">
|
||||
|
@ -194,37 +195,33 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
private renderQRReciprocatePhase() {
|
||||
const {member, request} = this.props;
|
||||
let Button;
|
||||
// a bit of a hack, but the FormButton should only be used in the right panel
|
||||
// they should probably just be the same component with a css class applied to it?
|
||||
if (this.props.inDialog) {
|
||||
Button = sdk.getComponent("elements.AccessibleButton");
|
||||
} else {
|
||||
Button = sdk.getComponent("elements.FormButton");
|
||||
}
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const description = request.isSelfVerification ?
|
||||
_t("Almost there! Is your other session showing the same shield?") :
|
||||
_t("Almost there! Is %(displayName)s showing the same shield?", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
let body: JSX.Element;
|
||||
if (this.state.reciprocateQREvent) {
|
||||
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
||||
//
|
||||
// we're passing both a label and a child string to Button as
|
||||
// FormButton and AccessibleButton expect this differently
|
||||
body = <React.Fragment>
|
||||
<p>{description}</p>
|
||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||
<Button
|
||||
label={_t("No")} kind="danger"
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateNoClick}>{_t("No")}</Button>
|
||||
<Button
|
||||
label={_t("Yes")} kind="primary"
|
||||
onClick={this.onReciprocateNoClick}
|
||||
>
|
||||
{ _t("No") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateYesClick}>{_t("Yes")}</Button>
|
||||
onClick={this.onReciprocateYesClick}
|
||||
>
|
||||
{ _t("Yes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
|
@ -264,7 +261,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
}
|
||||
} else {
|
||||
description = _t("You've successfully verified %(displayName)s!", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -302,7 +299,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
text = _t("You cancelled verification on your other session.");
|
||||
} else {
|
||||
text = _t("%(displayName)s cancelled verification.", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
text = `${text} ${startAgainInstruction}`;
|
||||
|
@ -325,7 +322,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
public render() {
|
||||
const {member, phase, request} = this.props;
|
||||
|
||||
const displayName = member.displayName || member.name || member.userId;
|
||||
const displayName = (member as User).displayName || (member as RoomMember).name || member.userId;
|
||||
|
||||
switch (phase) {
|
||||
case PHASE_READY:
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,59 +14,60 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, createRef } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import EditableItemList from "../elements/EditableItemList";
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import RoomPublishSetting from "./RoomPublishSetting";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
|
||||
class EditableAliasesList extends EditableItemList {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
interface IEditableAliasesListProps {
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
this._aliasField = createRef();
|
||||
}
|
||||
class EditableAliasesList extends EditableItemList<IEditableAliasesListProps> {
|
||||
private aliasField = createRef<RoomAliasField>();
|
||||
|
||||
_onAliasAdded = async () => {
|
||||
await this._aliasField.current.validate({ allowEmpty: false });
|
||||
private onAliasAdded = async () => {
|
||||
await this.aliasField.current.validate({ allowEmpty: false });
|
||||
|
||||
if (this._aliasField.current.isValid) {
|
||||
if (this.aliasField.current.isValid) {
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
return;
|
||||
}
|
||||
|
||||
this._aliasField.current.focus();
|
||||
this._aliasField.current.validate({ allowEmpty: false, focused: true });
|
||||
this.aliasField.current.focus();
|
||||
this.aliasField.current.validate({ allowEmpty: false, focused: true });
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
protected renderNewItemField() {
|
||||
// if we don't need the RoomAliasField,
|
||||
// we don't need to overriden version of _renderNewItemField
|
||||
// we don't need to overriden version of renderNewItemField
|
||||
if (!this.props.domain) {
|
||||
return super._renderNewItemField();
|
||||
return super.renderNewItemField();
|
||||
}
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
|
||||
const onChange = (alias) => this.onNewItemChanged({target: {value: alias}});
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._onAliasAdded}
|
||||
onSubmit={this.onAliasAdded}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EditableItemList_newItem"
|
||||
>
|
||||
<RoomAliasField
|
||||
ref={this._aliasField}
|
||||
ref={this.aliasField}
|
||||
onChange={onChange}
|
||||
value={this.props.newItem || ""}
|
||||
domain={this.props.domain} />
|
||||
<AccessibleButton onClick={this._onAliasAdded} kind="primary">
|
||||
<AccessibleButton onClick={this.onAliasAdded} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
@ -75,19 +75,30 @@ class EditableAliasesList extends EditableItemList {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.room_settings.AliasSettings")
|
||||
export default class AliasSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
||||
canSetAliases: PropTypes.bool.isRequired,
|
||||
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
||||
};
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
canSetCanonicalAlias: boolean;
|
||||
canSetAliases: boolean;
|
||||
canonicalAliasEvent?: MatrixEvent;
|
||||
hidePublishSetting?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
altAliases: string[];
|
||||
localAliases: string[];
|
||||
canonicalAlias?: string;
|
||||
updatingCanonicalAlias: boolean;
|
||||
localAliasesLoading: boolean;
|
||||
detailsOpen: boolean;
|
||||
newAlias?: string;
|
||||
newAltAlias?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.room_settings.AliasSettings")
|
||||
export default class AliasSettings extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
canSetAliases: false,
|
||||
canSetCanonicalAlias: false,
|
||||
aliasEvents: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -122,7 +133,7 @@ export default class AliasSettings extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async loadLocalAliases() {
|
||||
private async loadLocalAliases() {
|
||||
this.setState({ localAliasesLoading: true });
|
||||
try {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -134,12 +145,16 @@ export default class AliasSettings extends React.Component {
|
|||
}
|
||||
}
|
||||
this.setState({ localAliases });
|
||||
|
||||
if (localAliases.length === 0) {
|
||||
this.setState({ detailsOpen: true });
|
||||
}
|
||||
} finally {
|
||||
this.setState({ localAliasesLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
changeCanonicalAlias(alias) {
|
||||
private changeCanonicalAlias(alias: string) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
const oldAlias = this.state.canonicalAlias;
|
||||
|
@ -170,7 +185,7 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
changeAltAliases(altAliases) {
|
||||
private changeAltAliases(altAliases: string[]) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
this.setState({
|
||||
|
@ -181,7 +196,7 @@ export default class AliasSettings extends React.Component {
|
|||
const eventContent = {};
|
||||
|
||||
if (this.state.canonicalAlias) {
|
||||
eventContent.alias = this.state.canonicalAlias;
|
||||
eventContent["alias"] = this.state.canonicalAlias;
|
||||
}
|
||||
if (altAliases) {
|
||||
eventContent["alt_aliases"] = altAliases;
|
||||
|
@ -202,11 +217,11 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onNewAliasChanged = (value) => {
|
||||
this.setState({newAlias: value});
|
||||
private onNewAliasChanged = (value: string) => {
|
||||
this.setState({ newAlias: value });
|
||||
};
|
||||
|
||||
onLocalAliasAdded = (alias) => {
|
||||
private onLocalAliasAdded = (alias: string) => {
|
||||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
@ -232,7 +247,7 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onLocalAliasDeleted = (index) => {
|
||||
private onLocalAliasDeleted = (index: number) => {
|
||||
const alias = this.state.localAliases[index];
|
||||
// TODO: In future, we should probably be making sure that the alias actually belongs
|
||||
// to this room. See https://github.com/vector-im/element-web/issues/7353
|
||||
|
@ -261,7 +276,7 @@ export default class AliasSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onLocalAliasesToggled = (event) => {
|
||||
private onLocalAliasesToggled = (event: ChangeEvent<HTMLDetailsElement>) => {
|
||||
// expanded
|
||||
if (event.target.open) {
|
||||
// if local aliases haven't been preloaded yet at component mount
|
||||
|
@ -269,43 +284,45 @@ export default class AliasSettings extends React.Component {
|
|||
this.loadLocalAliases();
|
||||
}
|
||||
}
|
||||
this.setState({detailsOpen: event.target.open});
|
||||
this.setState({ detailsOpen: event.currentTarget.open });
|
||||
};
|
||||
|
||||
onCanonicalAliasChange = (event) => {
|
||||
private onCanonicalAliasChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.changeCanonicalAlias(event.target.value);
|
||||
};
|
||||
|
||||
onNewAltAliasChanged = (value) => {
|
||||
this.setState({newAltAlias: value});
|
||||
private onNewAltAliasChanged = (value: string) => {
|
||||
this.setState({ newAltAlias: value });
|
||||
}
|
||||
|
||||
onAltAliasAdded = (alias) => {
|
||||
private onAltAliasAdded = (alias: string) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
if (!altAliases.some(a => a.trim() === alias.trim())) {
|
||||
altAliases.push(alias.trim());
|
||||
this.changeAltAliases(altAliases);
|
||||
this.setState({newAltAlias: ""});
|
||||
this.setState({ newAltAlias: "" });
|
||||
}
|
||||
}
|
||||
|
||||
onAltAliasDeleted = (index) => {
|
||||
private onAltAliasDeleted = (index: number) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
altAliases.splice(index, 1);
|
||||
this.changeAltAliases(altAliases);
|
||||
}
|
||||
|
||||
_getAliases() {
|
||||
return this.state.altAliases.concat(this._getLocalNonAltAliases());
|
||||
private getAliases() {
|
||||
return this.state.altAliases.concat(this.getLocalNonAltAliases());
|
||||
}
|
||||
|
||||
_getLocalNonAltAliases() {
|
||||
private getLocalNonAltAliases() {
|
||||
const {altAliases} = this.state;
|
||||
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
|
||||
}
|
||||
|
||||
render() {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const localDomain = cli.getDomain();
|
||||
const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom();
|
||||
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
|
@ -320,7 +337,7 @@ export default class AliasSettings extends React.Component {
|
|||
>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
this._getAliases().map((alias, i) => {
|
||||
this.getAliases().map((alias, i) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i}>
|
||||
|
@ -340,12 +357,10 @@ export default class AliasSettings extends React.Component {
|
|||
|
||||
let localAliasesList;
|
||||
if (this.state.localAliasesLoading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
localAliasesList = <Spinner />;
|
||||
} else {
|
||||
localAliasesList = (<EditableAliasesList
|
||||
id="roomAliases"
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.localAliases}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
|
@ -353,7 +368,9 @@ export default class AliasSettings extends React.Component {
|
|||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
noItemsLabel={_t('This room has no local addresses')}
|
||||
noItemsLabel={isSpaceRoom
|
||||
? _t("This space has no local addresses")
|
||||
: _t("This room has no local addresses")}
|
||||
placeholder={_t('Local address')}
|
||||
domain={localDomain}
|
||||
/>);
|
||||
|
@ -362,18 +379,27 @@ export default class AliasSettings extends React.Component {
|
|||
return (
|
||||
<div className='mx_AliasSettings'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
|
||||
<p>{_t("Published addresses can be used by anyone on any server to join your room. " +
|
||||
"To publish an address, it needs to be set as a local address first.")}</p>
|
||||
{canonicalAliasSection}
|
||||
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
|
||||
<p>
|
||||
{ isSpaceRoom
|
||||
? _t("Published addresses can be used by anyone on any server to join your space.")
|
||||
: _t("Published addresses can be used by anyone on any server to join your room.")}
|
||||
|
||||
{ _t("To publish an address, it needs to be set as a local address first.") }
|
||||
</p>
|
||||
{ canonicalAliasSection }
|
||||
{ this.props.hidePublishSetting
|
||||
? null
|
||||
: <RoomPublishSetting
|
||||
roomId={this.props.roomId}
|
||||
canSetCanonicalAlias={this.props.canSetCanonicalAlias}
|
||||
/> }
|
||||
<datalist id="mx_AliasSettings_altRecommendations">
|
||||
{this._getLocalNonAltAliases().map(alias => {
|
||||
{this.getLocalNonAltAliases().map(alias => {
|
||||
return <option value={alias} key={alias} />;
|
||||
})};
|
||||
</datalist>
|
||||
<EditableAliasesList
|
||||
id="roomAltAliases"
|
||||
className={"mx_RoomSettings_altAliases"}
|
||||
items={this.state.altAliases}
|
||||
newItem={this.state.newAltAlias}
|
||||
onNewItemChanged={this.onNewAltAliasChanged}
|
||||
|
@ -386,11 +412,19 @@ export default class AliasSettings extends React.Component {
|
|||
noItemsLabel={_t('No other published addresses yet, add one below')}
|
||||
placeholder={_t('New published address (e.g. #alias:server)')}
|
||||
/>
|
||||
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span>
|
||||
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p>
|
||||
<details onToggle={this.onLocalAliasesToggled}>
|
||||
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>
|
||||
{ _t("Local Addresses") }
|
||||
</span>
|
||||
<p>
|
||||
{ isSpaceRoom
|
||||
? _t("Set addresses for this space so users can find this space " +
|
||||
"through your homeserver (%(localDomain)s)", { localDomain })
|
||||
: _t("Set addresses for this room so users can find this room " +
|
||||
"through your homeserver (%(localDomain)s)", { localDomain }) }
|
||||
</p>
|
||||
<details onToggle={this.onLocalAliasesToggled} open={this.state.detailsOpen}>
|
||||
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
|
||||
{localAliasesList}
|
||||
{ localAliasesList }
|
||||
</details>
|
||||
</div>
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 - 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.
|
||||
|
@ -14,20 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
label?: string;
|
||||
canSetCanonicalAlias?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isRoomPublished: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.room_settings.RoomPublishSetting")
|
||||
export default class RoomPublishSetting extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isRoomPublished: false};
|
||||
export default class RoomPublishSetting extends React.PureComponent<IProps, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRoomPublished: false,
|
||||
};
|
||||
}
|
||||
|
||||
onRoomPublishChange = (e) => {
|
||||
private onRoomPublishChange = (e) => {
|
||||
const valueBefore = this.state.isRoomPublished;
|
||||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
|
@ -52,11 +66,14 @@ export default class RoomPublishSetting extends React.PureComponent {
|
|||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
return (<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!this.props.canSetCanonicalAlias}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})} />);
|
||||
return (
|
||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!this.props.canSetCanonicalAlias}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -82,13 +82,6 @@ export default class AppsDrawer extends React.Component {
|
|||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
// Room has changed probably, update apps
|
||||
this._updateApps();
|
||||
}
|
||||
|
||||
onIsResizing = (resizing) => {
|
||||
// This one is the vertical, ie. change height of apps drawer
|
||||
this.setState({ resizingVertical: resizing });
|
||||
|
@ -141,7 +134,10 @@ export default class AppsDrawer extends React.Component {
|
|||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
// Room has changed, update apps
|
||||
this._updateApps();
|
||||
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||
this._loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
|
|
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