Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/ts/5.1
This commit is contained in:
commit
73b7610228
149 changed files with 4223 additions and 2289 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
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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]: "↑",
|
||||
|
|
|
@ -55,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.
|
||||
|
|
|
@ -32,13 +32,9 @@ 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);
|
||||
|
|
|
@ -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>,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -60,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";
|
||||
|
@ -139,7 +139,7 @@ export interface IState {
|
|||
draggingFile: boolean;
|
||||
searching: boolean;
|
||||
searchTerm?: string;
|
||||
searchScope?: "All" | "Room";
|
||||
searchScope?: SearchScope;
|
||||
searchResults?: XOR<{}, {
|
||||
count: number;
|
||||
highlights: string[];
|
||||
|
@ -1263,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,
|
||||
|
@ -1284,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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,7 +33,6 @@ 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 { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
export function canCancel(eventStatus) {
|
||||
|
@ -201,7 +200,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
onQuoteClick = () => {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
dis.dispatch({
|
||||
action: Action.ComposerInsert,
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
|
||||
|
@ -171,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();
|
||||
|
@ -195,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"
|
||||
|
@ -227,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") }
|
||||
|
|
|
@ -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}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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,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));
|
||||
|
|
|
@ -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";
|
||||
|
@ -503,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);
|
||||
|
@ -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") }
|
||||
|
|
|
@ -195,14 +195,7 @@ 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?", {
|
||||
|
@ -211,21 +204,24 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
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 {
|
||||
|
|
|
@ -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(),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import CallViewForRoom from '../voip/CallViewForRoom';
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import classNames from "classnames";
|
||||
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
|
@ -29,24 +28,24 @@ import { hasText } from "../../../TextForEvent";
|
|||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { formatTime } from "../../../DateUtils";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {E2E_STATE} from "./E2EIcon";
|
||||
import {toRem} from "../../../utils/units";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import { E2E_STATE } from "./E2EIcon";
|
||||
import { toRem } from "../../../utils/units";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import {objectHasDiff} from "../../../utils/objects";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { WIDGET_LAYOUT_EVENT_TYPE } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import { EditorStateTransfer } from "../../../utils/EditorStateTransfer";
|
||||
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
const eventTileTypes = {
|
||||
|
|
|
@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
||||
|
@ -466,6 +466,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
if (SettingsStore.getValue("feature_spaces")) return [];
|
||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||
// See https://github.com/vector-im/element-web/issues/14456
|
||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||
|
|
|
@ -45,7 +45,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
|
|||
import { Enable, Resizable } from "re-resizable";
|
||||
import { Direction } from "re-resizable/lib/resizer";
|
||||
import { polyfillTouchEvent } from "../../../@types/polyfill";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from "classnames";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.SearchBar")
|
||||
export default class SearchBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._search_term = createRef();
|
||||
|
||||
this.state = {
|
||||
scope: 'Room',
|
||||
};
|
||||
}
|
||||
|
||||
onThisRoomClick = () => {
|
||||
this.setState({ scope: 'Room' }, () => this._searchIfQuery());
|
||||
};
|
||||
|
||||
onAllRoomsClick = () => {
|
||||
this.setState({ scope: 'All' }, () => this._searchIfQuery());
|
||||
};
|
||||
|
||||
onSearchChange = (e) => {
|
||||
switch (e.key) {
|
||||
case Key.ENTER:
|
||||
this.onSearch();
|
||||
break;
|
||||
case Key.ESCAPE:
|
||||
this.props.onCancelClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
_searchIfQuery() {
|
||||
if (this._search_term.current.value) {
|
||||
this.onSearch();
|
||||
}
|
||||
}
|
||||
|
||||
onSearch = () => {
|
||||
this.props.onSearch(this._search_term.current.value, this.state.scope);
|
||||
};
|
||||
|
||||
render() {
|
||||
const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
|
||||
mx_SearchBar_searching: this.props.searchInProgress,
|
||||
});
|
||||
const thisRoomClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== 'Room',
|
||||
});
|
||||
const allRoomsClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== 'All',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx_SearchBar">
|
||||
<div className="mx_SearchBar_buttons" role="radiogroup">
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
|
||||
{_t("This Room")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
|
||||
{_t("All Rooms")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
|
||||
</div>
|
||||
<DesktopBuildsNotice isRoomEncrypted={this.props.isRoomEncrypted} kind={WarningKind.Search} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
130
src/components/views/rooms/SearchBar.tsx
Normal file
130
src/components/views/rooms/SearchBar.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, RefObject } from 'react';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from "classnames";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onCancelClick: () => void;
|
||||
onSearch: (query: string, scope: string) => void;
|
||||
searchInProgress?: boolean;
|
||||
isRoomEncrypted?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
scope: SearchScope;
|
||||
}
|
||||
|
||||
export enum SearchScope {
|
||||
Room = "Room",
|
||||
All = "All",
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.SearchBar")
|
||||
export default class SearchBar extends React.Component<IProps, IState> {
|
||||
private searchTerm: RefObject<HTMLInputElement> = createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
scope: SearchScope.Room,
|
||||
};
|
||||
}
|
||||
|
||||
private onThisRoomClick = () => {
|
||||
this.setState({ scope: SearchScope.Room }, () => this.searchIfQuery());
|
||||
};
|
||||
|
||||
private onAllRoomsClick = () => {
|
||||
this.setState({ scope: SearchScope.All }, () => this.searchIfQuery());
|
||||
};
|
||||
|
||||
private onSearchChange = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case Key.ENTER:
|
||||
this.onSearch();
|
||||
break;
|
||||
case Key.ESCAPE:
|
||||
this.props.onCancelClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private searchIfQuery(): void {
|
||||
if (this.searchTerm.current.value) {
|
||||
this.onSearch();
|
||||
}
|
||||
}
|
||||
|
||||
private onSearch = (): void => {
|
||||
this.props.onSearch(this.searchTerm.current.value, this.state.scope);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
|
||||
mx_SearchBar_searching: this.props.searchInProgress,
|
||||
});
|
||||
const thisRoomClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== SearchScope.Room,
|
||||
});
|
||||
const allRoomsClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== SearchScope.All,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx_SearchBar">
|
||||
<div className="mx_SearchBar_buttons" role="radiogroup">
|
||||
<AccessibleButton
|
||||
className={thisRoomClasses}
|
||||
onClick={this.onThisRoomClick}
|
||||
aria-checked={this.state.scope === SearchScope.Room}
|
||||
role="radio"
|
||||
>
|
||||
{_t("This Room")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={allRoomsClasses}
|
||||
onClick={this.onAllRoomsClick}
|
||||
aria-checked={this.state.scope === SearchScope.All}
|
||||
role="radio"
|
||||
>
|
||||
{_t("All Rooms")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
<input
|
||||
ref={this.searchTerm}
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
placeholder={_t("Search…")}
|
||||
onKeyDown={this.onSearchChange}
|
||||
/>
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
|
||||
</div>
|
||||
<DesktopBuildsNotice isRoomEncrypted={this.props.isRoomEncrypted} kind={WarningKind.Search} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
|||
import {MsgType} from "matrix-js-sdk/src/@types/event";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import CallMediaHandler from "../../../CallMediaHandler";
|
||||
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -77,7 +77,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
size: this.state.recorder.contentLength,
|
||||
},
|
||||
|
||||
// MSC1767 experiment
|
||||
// MSC1767 + Ideals of MSC2516 as MSC3245
|
||||
// https://github.com/matrix-org/matrix-doc/pull/3245
|
||||
"org.matrix.msc1767.text": "Voice message",
|
||||
"org.matrix.msc1767.file": {
|
||||
url: mxc,
|
||||
|
@ -88,14 +89,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
"org.matrix.msc1767.audio": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
|
||||
// Events can't have floats, so we try to maintain resolution by using 1024
|
||||
// as a maximum value. The waveform contains values between zero and 1, so this
|
||||
// should come out largely sane.
|
||||
//
|
||||
// We're expecting about one data point per second of audio.
|
||||
// https://github.com/matrix-org/matrix-doc/pull/3246
|
||||
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)),
|
||||
},
|
||||
"org.matrix.msc2516.voice": {}, // No content, this is a rendering hint
|
||||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||
});
|
||||
await this.disposeRecording();
|
||||
}
|
||||
|
@ -132,8 +129,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
// Do a sanity test to ensure we're about to grab a valid microphone reference. Things might
|
||||
// change between this and recording, but at least we will have tried.
|
||||
try {
|
||||
const devices = await CallMediaHandler.getDevices();
|
||||
if (!devices?.['audioinput']?.length) {
|
||||
const devices = await MediaDeviceHandler.getDevices();
|
||||
if (!devices?.['audioInput']?.length) {
|
||||
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
||||
title: _t("No microphone found"),
|
||||
description: <>
|
||||
|
|
|
@ -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.
|
||||
|
@ -15,68 +15,76 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../..";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
|
||||
import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
|
||||
import Modal from "../../../../../Modal";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
closeSettingsFn(): void;
|
||||
}
|
||||
|
||||
interface IRecommendedVersion {
|
||||
version: string;
|
||||
needsUpgrade: boolean;
|
||||
urgent: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
upgradeRecommendation?: IRecommendedVersion;
|
||||
oldRoomId?: string;
|
||||
oldEventId?: string;
|
||||
upgraded?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
|
||||
export default class AdvancedRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
export default class AdvancedRoomSettingsTab extends React.Component<IProps, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
// This is eventually set to the value of room.getRecommendedVersion()
|
||||
upgradeRecommendation: null,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
// we handle lack of this object gracefully later, so don't worry about it failing here.
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
room.getRecommendedVersion().then((v) => {
|
||||
const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
|
||||
const additionalStateChanges = {};
|
||||
const createEvent = room.currentState.getStateEvents("m.room.create", "");
|
||||
const additionalStateChanges: Partial<IState> = {};
|
||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
|
||||
if (predecessor && predecessor.room_id) {
|
||||
additionalStateChanges['oldRoomId'] = predecessor.room_id;
|
||||
additionalStateChanges['oldEventId'] = predecessor.event_id;
|
||||
additionalStateChanges['hasPreviousRoom'] = true;
|
||||
additionalStateChanges.oldRoomId = predecessor.room_id;
|
||||
additionalStateChanges.oldEventId = predecessor.event_id;
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
upgraded: tombstone && tombstone.getContent().replacement_room,
|
||||
upgraded: !!tombstone?.getContent().replacement_room,
|
||||
upgradeRecommendation: v,
|
||||
...additionalStateChanges,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_upgradeRoom = (e) => {
|
||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
||||
private upgradeRoom = (e) => {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
|
||||
};
|
||||
|
||||
_openDevtools = (e) => {
|
||||
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||
private openDevtools = (e) => {
|
||||
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
|
||||
};
|
||||
|
||||
_onOldRoomClicked = (e) => {
|
||||
private onOldRoomClicked = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -93,9 +101,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
let unfederatableSection;
|
||||
const createEvent = room.currentState.getStateEvents('m.room.create', '');
|
||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
|
||||
if (createEvent && createEvent.getContent()['m.federate'] === false) {
|
||||
unfederatableSection = <div>{_t('This room is not accessible by remote Matrix servers')}</div>;
|
||||
unfederatableSection = <div>{ _t('This room is not accessible by remote Matrix servers') }</div>;
|
||||
}
|
||||
|
||||
let roomUpgradeButton;
|
||||
|
@ -103,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
roomUpgradeButton = (
|
||||
<div>
|
||||
<p className='mx_SettingsTab_warningText'>
|
||||
{_t(
|
||||
{ _t(
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
|
||||
"to the new version of the room.</i> We'll post a link to the new room in the old " +
|
||||
"version of the room - room members will have to click this link to join the new room.",
|
||||
|
@ -111,51 +119,53 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
"b": (sub) => <b>{sub}</b>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
<AccessibleButton onClick={this._upgradeRoom} kind='primary'>
|
||||
{_t("Upgrade this room to the recommended room version")}
|
||||
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
|
||||
{ _t("Upgrade this room to the recommended room version") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let oldRoomLink;
|
||||
if (this.state.hasPreviousRoom) {
|
||||
if (this.state.oldRoomId) {
|
||||
let name = _t("this room");
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (room && room.name) name = room.name;
|
||||
oldRoomLink = (
|
||||
<AccessibleButton element='a' onClick={this._onOldRoomClicked}>
|
||||
{_t("View older messages in %(roomName)s.", {roomName: name})}
|
||||
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
|
||||
{ _t("View older messages in %(roomName)s.", { roomName: name }) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Room information")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>
|
||||
{ room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
|
||||
</span>
|
||||
<div>
|
||||
<span>{_t("Internal room ID:")}</span>
|
||||
{this.props.roomId}
|
||||
<span>{ _t("Internal room ID:") }</span>
|
||||
{ this.props.roomId }
|
||||
</div>
|
||||
{unfederatableSection}
|
||||
{ unfederatableSection }
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Room version")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Room version") }</span>
|
||||
<div>
|
||||
<span>{_t("Room version:")}</span>
|
||||
{room.getVersion()}
|
||||
<span>{ _t("Room version:") }</span>
|
||||
{ room.getVersion() }
|
||||
</div>
|
||||
{oldRoomLink}
|
||||
{roomUpgradeButton}
|
||||
{ oldRoomLink }
|
||||
{ roomUpgradeButton }
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
|
||||
<AccessibleButton onClick={this._openDevtools} kind='primary'>
|
||||
{_t("Open Devtools")}
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Developer options") }</span>
|
||||
<AccessibleButton onClick={this.openDevtools} kind='primary'>
|
||||
{ _t("Open Devtools") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
|
@ -60,7 +60,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
|
||||
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
|
||||
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
|
||||
const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
|
||||
|
||||
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
|
||||
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
|
||||
|
@ -100,7 +99,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<AliasSettings roomId={this.props.roomId}
|
||||
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
|
||||
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
|
||||
canonicalAliasEvent={canonicalAliasEv} />
|
||||
</div>
|
||||
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
|
||||
{ flairSection }
|
||||
|
|
|
@ -29,19 +29,19 @@ import {UIFeature} from "../../../../../settings/UIFeature";
|
|||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
||||
// Knock and private are reserved keywords which are not yet implemented.
|
||||
enum JoinRule {
|
||||
export enum JoinRule {
|
||||
Public = "public",
|
||||
Knock = "knock",
|
||||
Invite = "invite",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
enum GuestAccess {
|
||||
export enum GuestAccess {
|
||||
CanJoin = "can_join",
|
||||
Forbidden = "forbidden",
|
||||
}
|
||||
|
||||
enum HistoryVisibility {
|
||||
export enum HistoryVisibility {
|
||||
Invited = "invited",
|
||||
Joined = "joined",
|
||||
Shared = "shared",
|
||||
|
@ -121,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
|
||||
};
|
||||
|
||||
private onEncryptionChange = (e: React.ChangeEvent) => {
|
||||
private onEncryptionChange = () => {
|
||||
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
|
||||
title: _t('Enable encryption?'),
|
||||
description: _t(
|
||||
|
|
|
@ -32,7 +32,7 @@ import * as ContextMenu from "../../../../structures/ContextMenu";
|
|||
import { toRightOf } from "../../../../structures/ContextMenu";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn: () => {};
|
||||
closeSettingsFn: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import CallMediaHandler from "../../../../../CallMediaHandler";
|
||||
import MediaDeviceHandler from "../../../../../MediaDeviceHandler";
|
||||
import Field from "../../../elements/Field";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||
|
@ -41,7 +41,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
|
||||
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
||||
if (canSeeDeviceLabels) {
|
||||
this._refreshMediaDevices();
|
||||
}
|
||||
|
@ -49,10 +49,10 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
|
||||
_refreshMediaDevices = async (stream) => {
|
||||
this.setState({
|
||||
mediaDevices: await CallMediaHandler.getDevices(),
|
||||
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
||||
activeAudioInput: CallMediaHandler.getAudioInput(),
|
||||
activeVideoInput: CallMediaHandler.getVideoInput(),
|
||||
mediaDevices: await MediaDeviceHandler.getDevices(),
|
||||
activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
|
||||
activeAudioInput: MediaDeviceHandler.getAudioInput(),
|
||||
activeVideoInput: MediaDeviceHandler.getVideoInput(),
|
||||
});
|
||||
if (stream) {
|
||||
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
||||
|
@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
};
|
||||
|
||||
_setAudioOutput = (e) => {
|
||||
CallMediaHandler.setAudioOutput(e.target.value);
|
||||
MediaDeviceHandler.instance.setAudioOutput(e.target.value);
|
||||
this.setState({
|
||||
activeAudioOutput: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_setAudioInput = (e) => {
|
||||
CallMediaHandler.setAudioInput(e.target.value);
|
||||
MediaDeviceHandler.instance.setAudioInput(e.target.value);
|
||||
this.setState({
|
||||
activeAudioInput: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_setVideoInput = (e) => {
|
||||
CallMediaHandler.setVideoInput(e.target.value);
|
||||
MediaDeviceHandler.instance.setVideoInput(e.target.value);
|
||||
this.setState({
|
||||
activeVideoInput: e.target.value,
|
||||
});
|
||||
|
@ -171,7 +171,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
|
||||
const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
|
||||
if (audioOutputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(audioOutputs);
|
||||
speakerDropdown = (
|
||||
|
@ -183,7 +183,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
|
||||
const audioInputs = this.state.mediaDevices.audioInput.slice(0);
|
||||
if (audioInputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(audioInputs);
|
||||
microphoneDropdown = (
|
||||
|
@ -195,7 +195,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const videoInputs = this.state.mediaDevices.videoinput.slice(0);
|
||||
const videoInputs = this.state.mediaDevices.videoInput.slice(0);
|
||||
if (videoInputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(videoInputs);
|
||||
webcamDropdown = (
|
||||
|
|
|
@ -29,12 +29,13 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import {BetaPill} from "../beta/BetaCard";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
|
||||
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||
import Field from "../elements/Field";
|
||||
import withValidation from "../elements/Validation";
|
||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
|
||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||
return (
|
||||
|
@ -60,6 +61,11 @@ const spaceNameValidator = withValidation({
|
|||
],
|
||||
});
|
||||
|
||||
const nameToAlias = (name: string, domain: string): string => {
|
||||
const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
|
||||
return `#${localpart}:${domain}`;
|
||||
};
|
||||
|
||||
const SpaceCreateMenu = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||
|
@ -67,6 +73,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [alias, setAlias] = useState("");
|
||||
const spaceAliasField = useRef<RoomAliasField>();
|
||||
const [avatar, setAvatar] = useState<File>(null);
|
||||
const [topic, setTopic] = useState<string>("");
|
||||
|
||||
|
@ -82,6 +90,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const initialState: ICreateRoomStateEvent[] = [
|
||||
{
|
||||
|
@ -99,12 +114,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
content: { url },
|
||||
});
|
||||
}
|
||||
if (topic) {
|
||||
initialState.push({
|
||||
type: EventType.RoomTopic,
|
||||
content: { topic },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await createRoom({
|
||||
|
@ -112,7 +121,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
creation_content: {
|
||||
// Based on MSC1840
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
initial_state: initialState,
|
||||
|
@ -121,6 +129,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
events_default: 100,
|
||||
...Visibility.Public ? { invite: 0 } : {},
|
||||
},
|
||||
room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
|
||||
topic,
|
||||
},
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
|
@ -159,6 +169,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
<SpaceFeedbackPrompt onClick={onFinished} />
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const domain = cli.getDomain();
|
||||
body = <React.Fragment>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_SpaceCreateMenu_back"
|
||||
|
@ -187,12 +198,30 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
label={_t("Name")}
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={ev => setName(ev.target.value)}
|
||||
onChange={ev => {
|
||||
const newName = ev.target.value;
|
||||
if (!alias || alias === nameToAlias(name, domain)) {
|
||||
setAlias(nameToAlias(newName, domain));
|
||||
}
|
||||
setName(newName);
|
||||
}}
|
||||
ref={spaceNameField}
|
||||
onValidate={spaceNameValidator}
|
||||
disabled={busy}
|
||||
/>
|
||||
|
||||
{ visibility === Visibility.Public
|
||||
? <RoomAliasField
|
||||
ref={spaceAliasField}
|
||||
onChange={setAlias}
|
||||
domain={domain}
|
||||
value={alias}
|
||||
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
|
||||
label={_t("Address")}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
||||
<Field
|
||||
name="spaceTopic"
|
||||
element="textarea"
|
||||
|
@ -224,7 +253,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
onFinished();
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: USER_LABS_TAB,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
}} />
|
||||
{ body }
|
||||
|
|
|
@ -14,18 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
|
||||
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {useContextMenu} from "../../structures/ContextMenu";
|
||||
import { useContextMenu } from "../../structures/ContextMenu";
|
||||
import SpaceCreateMenu from "./SpaceCreateMenu";
|
||||
import {SpaceItem} from "./SpaceTreeLevel";
|
||||
import { SpaceItem } from "./SpaceTreeLevel";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore, {
|
||||
HOME_SPACE,
|
||||
UPDATE_INVITED_SPACES,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
UPDATE_TOP_LEVEL_SPACES,
|
||||
|
@ -37,9 +39,10 @@ import {
|
|||
RovingAccessibleTooltipButton,
|
||||
RovingTabIndexProvider,
|
||||
} from "../../../accessibility/RovingTabIndex";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import {NotificationState} from "../../../stores/notifications/NotificationState";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IButtonProps {
|
||||
space?: Room;
|
||||
|
@ -120,11 +123,65 @@ const useSpaces = (): [Room[], Room[], Room | null] => {
|
|||
return [invites, spaces, activeSpace];
|
||||
};
|
||||
|
||||
interface IInnerSpacePanelProps {
|
||||
children?: ReactNode;
|
||||
isPanelCollapsed: boolean;
|
||||
setPanelCollapsed: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
|
||||
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => {
|
||||
const [invites, spaces, activeSpace] = useSpaces();
|
||||
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||
|
||||
const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
|
||||
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
|
||||
|
||||
return <div className="mx_SpaceTreeLevel">
|
||||
<SpaceButton
|
||||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
selected={!activeSpace}
|
||||
tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
|
||||
notificationState={homeNotificationState}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
{ invites.map(s => (
|
||||
<SpaceItem
|
||||
key={s.roomId}
|
||||
space={s}
|
||||
activeSpaces={activeSpaces}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
onExpand={() => setPanelCollapsed(false)}
|
||||
/>
|
||||
)) }
|
||||
{ spaces.map((s, i) => (
|
||||
<Draggable key={s.roomId} draggableId={s.roomId} index={i}>
|
||||
{(provided, snapshot) => (
|
||||
<SpaceItem
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
key={s.roomId}
|
||||
innerRef={provided.innerRef}
|
||||
className={snapshot.isDragging
|
||||
? "mx_SpaceItem_dragging"
|
||||
: undefined}
|
||||
space={s}
|
||||
activeSpaces={activeSpaces}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
onExpand={() => setPanelCollapsed(false)}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
)) }
|
||||
{ children }
|
||||
</div>;
|
||||
});
|
||||
|
||||
const SpacePanel = () => {
|
||||
// We don't need the handle as we position the menu in a constant location
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||
const [invites, spaces, activeSpace] = useSpaces();
|
||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -133,10 +190,6 @@ const SpacePanel = () => {
|
|||
}
|
||||
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const newClasses = classNames("mx_SpaceButton_new", {
|
||||
mx_SpaceButton_newCancel: menuDisplayed,
|
||||
});
|
||||
|
||||
let contextMenu = null;
|
||||
if (menuDisplayed) {
|
||||
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
|
||||
|
@ -203,59 +256,61 @@ const SpacePanel = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
|
||||
// TODO drag and drop for re-arranging order
|
||||
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({onKeyDownHandler}) => (
|
||||
<ul
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
>
|
||||
<AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
|
||||
<div className="mx_SpaceTreeLevel">
|
||||
<SpaceButton
|
||||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
selected={!activeSpace}
|
||||
tooltip={_t("All rooms")}
|
||||
notificationState={RoomNotificationStateStore.instance.globalState}
|
||||
isNarrow={isPanelCollapsed}
|
||||
const onNewClick = menuDisplayed ? closeMenu : () => {
|
||||
if (!isPanelCollapsed) setPanelCollapsed(true);
|
||||
openMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={result => {
|
||||
if (!result.destination) return; // dropped outside the list
|
||||
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
|
||||
}}>
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({onKeyDownHandler}) => (
|
||||
<ul
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
>
|
||||
<Droppable droppableId="top-level-spaces">
|
||||
{(provided, snapshot) => (
|
||||
<AutoHideScrollbar
|
||||
{...provided.droppableProps}
|
||||
wrappedRef={provided.innerRef}
|
||||
className="mx_SpacePanel_spaceTreeWrapper"
|
||||
style={snapshot.isDraggingOver ? {
|
||||
pointerEvents: "none",
|
||||
} : undefined}
|
||||
>
|
||||
<InnerSpacePanel
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
setPanelCollapsed={setPanelCollapsed}
|
||||
>
|
||||
{ provided.placeholder }
|
||||
</InnerSpacePanel>
|
||||
|
||||
<SpaceButton
|
||||
className={classNames("mx_SpaceButton_new", {
|
||||
mx_SpaceButton_newCancel: menuDisplayed,
|
||||
})}
|
||||
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||
onClick={onNewClick}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
</AutoHideScrollbar>
|
||||
)}
|
||||
</Droppable>
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_SpacePanel_toggleCollapse", { expanded: !isPanelCollapsed })}
|
||||
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
||||
title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
|
||||
/>
|
||||
{ invites.map(s => <SpaceItem
|
||||
key={s.roomId}
|
||||
space={s}
|
||||
activeSpaces={activeSpaces}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
onExpand={() => setPanelCollapsed(false)}
|
||||
/>) }
|
||||
{ spaces.map(s => <SpaceItem
|
||||
key={s.roomId}
|
||||
space={s}
|
||||
activeSpaces={activeSpaces}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
onExpand={() => setPanelCollapsed(false)}
|
||||
/>) }
|
||||
</div>
|
||||
<SpaceButton
|
||||
className={newClasses}
|
||||
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||
onClick={menuDisplayed ? closeMenu : () => {
|
||||
if (!isPanelCollapsed) setPanelCollapsed(true);
|
||||
openMenu();
|
||||
}}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
</AutoHideScrollbar>
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
||||
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
||||
title={expandCollapseButtonTitle}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</ul>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
{ contextMenu }
|
||||
</ul>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpacePanel;
|
||||
|
|
143
src/components/views/spaces/SpaceSettingsGeneralTab.tsx
Normal file
143
src/components/views/spaces/SpaceSettingsGeneralTab.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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 { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
|
||||
import SpaceBasicSettings from "./SpaceBasicSettings";
|
||||
import { avatarUrlForRoom } from "../../../Avatar";
|
||||
import { IDialogProps } from "../dialogs/IDialogProps";
|
||||
import { getTopic } from "../elements/RoomTopic";
|
||||
import { defaultDispatcher } from "../../../dispatcher/dispatcher";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
}
|
||||
|
||||
const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProps) => {
|
||||
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 onCancel = () => {
|
||||
setNewAvatar(null);
|
||||
setName(space.name);
|
||||
setTopic(currentTopic);
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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."));
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||
|
||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<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}
|
||||
/>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={onCancel}
|
||||
disabled={busy || !(avatarChanged || nameChanged || topicChanged)}
|
||||
kind="link"
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
||||
{ busy ? _t("Saving...") : _t("Save Changes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Leave Space")}</span>
|
||||
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("Leave Space") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsGeneralTab;
|
187
src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
Normal file
187
src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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 { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AliasSettings from "../room_settings/AliasSettings";
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
}
|
||||
|
||||
enum SpaceVisibility {
|
||||
Unlisted = "unlisted",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
const useLocalEcho = <T extends any>(
|
||||
currentFactory: () => T,
|
||||
setterFn: (value: T) => Promise<void>,
|
||||
errorFn: (error: Error) => void,
|
||||
): [value: T, handler: (value: T) => void] => {
|
||||
const [value, setValue] = useState(currentFactory);
|
||||
const handler = async (value: T) => {
|
||||
setValue(value);
|
||||
try {
|
||||
await setterFn(value);
|
||||
} catch (e) {
|
||||
setValue(currentFactory());
|
||||
errorFn(e);
|
||||
}
|
||||
};
|
||||
|
||||
return [value, handler];
|
||||
};
|
||||
|
||||
const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [visibility, setVisibility] = useLocalEcho<SpaceVisibility>(
|
||||
() => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
|
||||
visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
|
||||
join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private,
|
||||
}, ""),
|
||||
() => setError(_t("Failed to update the visibility of this space")),
|
||||
);
|
||||
const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho<boolean>(
|
||||
() => space.currentState.getStateEvents(EventType.RoomGuestAccess, "")
|
||||
?.getContent()?.guest_access === GuestAccess.CanJoin,
|
||||
guestAccessEnabled => cli.sendStateEvent(space.roomId, EventType.RoomGuestAccess, {
|
||||
guest_access: guestAccessEnabled ? GuestAccess.CanJoin : GuestAccess.Forbidden,
|
||||
}, ""),
|
||||
() => setError(_t("Failed to update the guest access of this space")),
|
||||
);
|
||||
const [historyVisibility, setHistoryVisibility] = useLocalEcho<HistoryVisibility>(
|
||||
() => space.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")
|
||||
?.getContent()?.history_visibility || HistoryVisibility.Shared,
|
||||
historyVisibility => cli.sendStateEvent(space.roomId, EventType.RoomHistoryVisibility, {
|
||||
history_visibility: historyVisibility,
|
||||
}, ""),
|
||||
() => setError(_t("Failed to update the history visibility of this space")),
|
||||
);
|
||||
|
||||
const [showAdvancedSection, toggleAdvancedSection] = useStateToggle();
|
||||
|
||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||
const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId);
|
||||
const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId);
|
||||
const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli);
|
||||
const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
|
||||
|
||||
let advancedSection;
|
||||
if (showAdvancedSection) {
|
||||
advancedSection = <>
|
||||
<AccessibleButton onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced">
|
||||
{ _t("Hide advanced") }
|
||||
</AccessibleButton>
|
||||
|
||||
<LabelledToggleSwitch
|
||||
value={guestAccessEnabled}
|
||||
onChange={setGuestAccessEnabled}
|
||||
disabled={!canSetGuestAccess}
|
||||
label={_t("Enable guest access")}
|
||||
/>
|
||||
<p>
|
||||
{ _t("Guests can join a space without having an account.") }
|
||||
<br />
|
||||
{ _t("This may be useful for public spaces.") }
|
||||
</p>
|
||||
</>;
|
||||
} else {
|
||||
advancedSection = <>
|
||||
<AccessibleButton onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced">
|
||||
{ _t("Show advanced") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
let addressesSection;
|
||||
if (visibility !== SpaceVisibility.Private) {
|
||||
addressesSection = <>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Address")}</span>
|
||||
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||
<AliasSettings
|
||||
roomId={space.roomId}
|
||||
canSetCanonicalAlias={canSetCanonical}
|
||||
canSetAliases={true}
|
||||
canonicalAliasEvent={canonicalAliasEv}
|
||||
hidePublishSetting={true}
|
||||
/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Visibility") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<div className="mx_SettingsTab_section_caption">
|
||||
{ _t("Decide who can view and join %(spaceName)s.", { spaceName: space.name }) }
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<StyledRadioGroup
|
||||
name="spaceVisibility"
|
||||
value={visibility}
|
||||
onChange={setVisibility}
|
||||
disabled={!canSetJoinRule}
|
||||
definitions={[
|
||||
{
|
||||
value: SpaceVisibility.Unlisted,
|
||||
label: _t("Public"),
|
||||
description: _t("anyone with the link can view and join"),
|
||||
}, {
|
||||
value: SpaceVisibility.Private,
|
||||
label: _t("Invite only"),
|
||||
description: _t("only invited people can view and join"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ advancedSection }
|
||||
|
||||
<LabelledToggleSwitch
|
||||
value={historyVisibility === HistoryVisibility.WorldReadable}
|
||||
onChange={(checked: boolean) => {
|
||||
setHistoryVisibility(checked ? HistoryVisibility.WorldReadable : HistoryVisibility.Shared);
|
||||
}}
|
||||
disabled={!canSetHistoryVisibility}
|
||||
label={_t("Preview Space")}
|
||||
/>
|
||||
<div>{ _t("Allow people to preview your space before they join.") }</div>
|
||||
<b>{ _t("Recommended for public spaces.") }</b>
|
||||
</div>
|
||||
|
||||
{ addressesSection }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsVisibilityTab;
|
|
@ -14,23 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { createRef, InputHTMLAttributes, LegacyRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
|
||||
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import {
|
||||
shouldShowSpaceSettings,
|
||||
showAddExistingRooms,
|
||||
|
@ -39,33 +38,38 @@ import {
|
|||
showSpaceSettings,
|
||||
} from "../../../utils/space";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||
import {NotificationColor} from "../../../stores/notifications/NotificationColor";
|
||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
|
||||
|
||||
interface IItemProps {
|
||||
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
|
||||
space?: Room;
|
||||
activeSpaces: Room[];
|
||||
isNested?: boolean;
|
||||
isPanelCollapsed?: boolean;
|
||||
onExpand?: Function;
|
||||
parents?: Set<string>;
|
||||
innerRef?: LegacyRef<HTMLLIElement>;
|
||||
}
|
||||
|
||||
interface IItemState {
|
||||
collapsed: boolean;
|
||||
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
|
||||
childSpaces: Room[];
|
||||
}
|
||||
|
||||
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private buttonRef = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -78,14 +82,36 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
this.state = {
|
||||
collapsed: collapsed,
|
||||
contextMenuPosition: null,
|
||||
childSpaces: this.childSpaces,
|
||||
};
|
||||
|
||||
SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
|
||||
}
|
||||
|
||||
private toggleCollapse(evt) {
|
||||
if (this.props.onExpand && this.state.collapsed) {
|
||||
componentWillUnmount() {
|
||||
SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
|
||||
}
|
||||
|
||||
private onSpaceUpdate = () => {
|
||||
this.setState({
|
||||
childSpaces: this.childSpaces,
|
||||
});
|
||||
};
|
||||
|
||||
private get childSpaces() {
|
||||
return SpaceStore.instance.getChildSpaces(this.props.space.roomId)
|
||||
.filter(s => !this.props.parents?.has(s.roomId));
|
||||
}
|
||||
|
||||
private get isCollapsed() {
|
||||
return this.state.collapsed || this.props.isPanelCollapsed;
|
||||
}
|
||||
|
||||
private toggleCollapse = evt => {
|
||||
if (this.props.onExpand && this.isCollapsed) {
|
||||
this.props.onExpand();
|
||||
}
|
||||
const newCollapsedState = !this.state.collapsed;
|
||||
const newCollapsedState = !this.isCollapsed;
|
||||
|
||||
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
|
||||
this.props.space.roomId,
|
||||
|
@ -96,7 +122,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
// don't bubble up so encapsulating button for space
|
||||
// doesn't get triggered
|
||||
evt.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
private onContextMenu = (ev: React.MouseEvent) => {
|
||||
if (this.props.space.getMyMembership() !== "join") return;
|
||||
|
@ -111,6 +137,43 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
});
|
||||
}
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
const action = getKeyBindingsManager().getRoomListAction(ev);
|
||||
const hasChildren = this.state.childSpaces?.length;
|
||||
switch (action) {
|
||||
case RoomListAction.CollapseSection:
|
||||
if (hasChildren && !this.isCollapsed) {
|
||||
this.toggleCollapse(ev);
|
||||
} else {
|
||||
const parentItem = this.buttonRef?.current?.parentElement?.parentElement;
|
||||
const parentButton = parentItem?.previousElementSibling as HTMLElement;
|
||||
parentButton?.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case RoomListAction.ExpandSection:
|
||||
if (hasChildren) {
|
||||
if (this.isCollapsed) {
|
||||
this.toggleCollapse(ev);
|
||||
} else {
|
||||
const childLevel = this.buttonRef?.current?.nextElementSibling;
|
||||
const firstSpaceItemChild = childLevel?.querySelector<HTMLLIElement>(".mx_SpaceItem");
|
||||
firstSpaceItemChild?.querySelector<HTMLDivElement>(".mx_SpaceButton")?.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -300,27 +363,25 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {space, activeSpaces, isNested} = this.props;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
|
||||
...otherProps } = this.props;
|
||||
|
||||
const forceCollapsed = this.props.isPanelCollapsed;
|
||||
const isNarrow = this.props.isPanelCollapsed;
|
||||
const collapsed = this.state.collapsed || forceCollapsed;
|
||||
const collapsed = this.isCollapsed;
|
||||
|
||||
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
|
||||
.filter(s => !this.props.parents?.has(s.roomId));
|
||||
const isActive = activeSpaces.includes(space);
|
||||
const itemClasses = classNames({
|
||||
const itemClasses = classNames(this.props.className, {
|
||||
"mx_SpaceItem": true,
|
||||
"mx_SpaceItem_narrow": isNarrow,
|
||||
"mx_SpaceItem_narrow": isPanelCollapsed,
|
||||
"collapsed": collapsed,
|
||||
"hasSubSpaces": childSpaces && childSpaces.length,
|
||||
"hasSubSpaces": this.state.childSpaces?.length,
|
||||
});
|
||||
|
||||
const isInvite = space.getMyMembership() === "invite";
|
||||
const classes = classNames("mx_SpaceButton", {
|
||||
mx_SpaceButton_active: isActive,
|
||||
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||
mx_SpaceButton_narrow: isNarrow,
|
||||
mx_SpaceButton_narrow: isPanelCollapsed,
|
||||
mx_SpaceButton_invite: isInvite,
|
||||
});
|
||||
const notificationState = isInvite
|
||||
|
@ -328,12 +389,12 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
: SpaceStore.instance.getNotificationState(space.roomId);
|
||||
|
||||
let childItems;
|
||||
if (childSpaces && !collapsed) {
|
||||
if (this.state.childSpaces?.length && !collapsed) {
|
||||
childItems = <SpaceTreeLevel
|
||||
spaces={childSpaces}
|
||||
spaces={this.state.childSpaces}
|
||||
activeSpaces={activeSpaces}
|
||||
isNested={true}
|
||||
parents={new Set(this.props.parents).add(this.props.space.roomId)}
|
||||
parents={new Set(parents).add(space.roomId)}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -346,53 +407,36 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
|
||||
const avatarSize = isNested ? 24 : 32;
|
||||
|
||||
const toggleCollapseButton = childSpaces && childSpaces.length ?
|
||||
const toggleCollapseButton = this.state.childSpaces?.length ?
|
||||
<AccessibleButton
|
||||
className="mx_SpaceButton_toggleCollapse"
|
||||
onClick={evt => this.toggleCollapse(evt)}
|
||||
onClick={this.toggleCollapse}
|
||||
tabIndex={-1}
|
||||
aria-label={collapsed ? _t("Expand") : _t("Collapse")}
|
||||
/> : null;
|
||||
|
||||
let button;
|
||||
if (isNarrow) {
|
||||
button = (
|
||||
return (
|
||||
<li {...otherProps} className={itemClasses} ref={innerRef}>
|
||||
<RovingAccessibleTooltipButton
|
||||
className={classes}
|
||||
title={space.name}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
forceHide={!!this.state.contextMenuPosition}
|
||||
forceHide={!isPanelCollapsed || !!this.state.contextMenuPosition}
|
||||
role="treeitem"
|
||||
aria-expanded={!collapsed}
|
||||
inputRef={this.buttonRef}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ toggleCollapseButton }
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
{ !isPanelCollapsed && <span className="mx_SpaceButton_name">{ space.name }</span> }
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<RovingAccessibleButton
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
role="treeitem"
|
||||
>
|
||||
{ toggleCollapseButton }
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
<span className="mx_SpaceButton_name">{ space.name }</span>
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={itemClasses}>
|
||||
{ button }
|
||||
{ childItems }
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {ReactNode} from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
import FormButton from "../elements/FormButton";
|
||||
import {XOR} from "../../../@types/common";
|
||||
|
||||
export interface IProps {
|
||||
|
@ -50,8 +50,12 @@ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
|
|||
{detailContent}
|
||||
</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
{onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> }
|
||||
<FormButton label={acceptLabel} onClick={onAccept} />
|
||||
{onReject && rejectLabel && <AccessibleButton kind="danger" onClick={onReject}>
|
||||
{ rejectLabel }
|
||||
</AccessibleButton> }
|
||||
<AccessibleButton onClick={onAccept} kind="primary">
|
||||
{ acceptLabel }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import CallMediaHandler from "../../../CallMediaHandler";
|
||||
import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
|
||||
|
||||
interface IProps {
|
||||
feed: CallFeed,
|
||||
|
@ -27,19 +27,25 @@ export default class AudioFeed extends React.Component<IProps> {
|
|||
private element = createRef<HTMLAudioElement>();
|
||||
|
||||
componentDidMount() {
|
||||
MediaDeviceHandler.instance.addListener(
|
||||
MediaDeviceHandlerEvent.AudioOutputChanged,
|
||||
this.onAudioOutputChanged,
|
||||
);
|
||||
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.playMedia();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
MediaDeviceHandler.instance.removeListener(
|
||||
MediaDeviceHandlerEvent.AudioOutputChanged,
|
||||
this.onAudioOutputChanged,
|
||||
);
|
||||
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.stopMedia();
|
||||
}
|
||||
|
||||
private playMedia() {
|
||||
private onAudioOutputChanged = (audioOutput: string) => {
|
||||
const element = this.element.current;
|
||||
const audioOutput = CallMediaHandler.getAudioOutput();
|
||||
|
||||
if (audioOutput) {
|
||||
try {
|
||||
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
||||
|
@ -52,7 +58,11 @@ export default class AudioFeed extends React.Component<IProps> {
|
|||
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private playMedia() {
|
||||
const element = this.element.current;
|
||||
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
|
||||
element.muted = false;
|
||||
element.srcObject = this.props.feed.stream;
|
||||
element.autoplay = true;
|
||||
|
|
|
@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler';
|
|||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import CallHandler, { AudioID } from '../../../CallHandler';
|
||||
import RoomAvatar from '../avatars/RoomAvatar';
|
||||
import FormButton from '../elements/FormButton';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||
|
@ -143,21 +143,22 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>
|
||||
<div className="mx_IncomingCallBox_buttons">
|
||||
<FormButton
|
||||
<AccessibleButton
|
||||
className={"mx_IncomingCallBox_decline"}
|
||||
onClick={this.onRejectClick}
|
||||
kind="danger"
|
||||
label={_t("Decline")}
|
||||
/>
|
||||
>
|
||||
{ _t("Decline") }
|
||||
</AccessibleButton>
|
||||
<div className="mx_IncomingCallBox_spacer" />
|
||||
<FormButton
|
||||
<AccessibleButton
|
||||
className={"mx_IncomingCallBox_accept"}
|
||||
onClick={this.onAnswerClick}
|
||||
kind="primary"
|
||||
label={_t("Accept")}
|
||||
/>
|
||||
>
|
||||
{ _t("Accept") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -164,4 +164,9 @@ export enum Action {
|
|||
* Inserts content into the active composer. Should be used with ComposerInsertPayload
|
||||
*/
|
||||
ComposerInsert = "composer_insert",
|
||||
|
||||
/**
|
||||
* Switches space. Should be used with SwitchSpacePayload.
|
||||
*/
|
||||
SwitchSpace = "switch_space",
|
||||
}
|
||||
|
|
27
src/dispatcher/payloads/SwitchSpacePayload.ts
Normal file
27
src/dispatcher/payloads/SwitchSpacePayload.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { ActionPayload } from "../payloads";
|
||||
import { Action } from "../actions";
|
||||
|
||||
export interface SwitchSpacePayload extends ActionPayload {
|
||||
action: Action.SwitchSpace;
|
||||
|
||||
/**
|
||||
* The number of the space to switch to, 1-indexed, 0 is Home.
|
||||
*/
|
||||
num: number;
|
||||
}
|
46
src/hooks/useRoomState.ts
Normal file
46
src/hooks/useRoomState.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
|
||||
type Mapper<T> = (roomState: RoomState) => T;
|
||||
const defaultMapper: Mapper<RoomState> = (roomState: RoomState) => roomState;
|
||||
|
||||
// Hook to simplify watching Matrix Room state
|
||||
export const useRoomState = <T extends any = RoomState>(
|
||||
room: Room,
|
||||
mapper: Mapper<T> = defaultMapper as Mapper<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;
|
||||
};
|
|
@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react";
|
|||
|
||||
// Hook to simplify toggling of a boolean state value
|
||||
// Returns value, method to toggle boolean value and method to set the boolean value
|
||||
export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch<SetStateAction<boolean>>] => {
|
||||
export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch<SetStateAction<boolean>>] => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const toggleValue = () => {
|
||||
setValue(!value);
|
||||
|
|
|
@ -784,6 +784,7 @@
|
|||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||
"Change notification settings": "Change notification settings",
|
||||
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
|
||||
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
|
||||
"Spaces": "Spaces",
|
||||
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
|
||||
|
@ -793,6 +794,10 @@
|
|||
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
|
||||
"Show all rooms in Home": "Show all rooms in Home",
|
||||
"Show people in spaces": "Show people in spaces",
|
||||
"If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.",
|
||||
"Show notification badges for People in Spaces": "Show notification badges for People in Spaces",
|
||||
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
|
||||
"Send and receive voice messages": "Send and receive voice messages",
|
||||
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||
|
@ -1018,17 +1023,42 @@
|
|||
"Your private space": "Your private space",
|
||||
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
|
||||
"You can change these anytime.": "You can change these anytime.",
|
||||
"e.g. my-space": "e.g. my-space",
|
||||
"Address": "Address",
|
||||
"Creating...": "Creating...",
|
||||
"Create": "Create",
|
||||
"All rooms": "All rooms",
|
||||
"Home": "Home",
|
||||
"Expand space panel": "Expand space panel",
|
||||
"Collapse space panel": "Collapse space panel",
|
||||
"All rooms": "All rooms",
|
||||
"Click to copy": "Click to copy",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
"Share invite link": "Share invite link",
|
||||
"Invite people": "Invite people",
|
||||
"Invite with email or username": "Invite with email or username",
|
||||
"Failed to save space settings.": "Failed to save space settings.",
|
||||
"General": "General",
|
||||
"Edit settings relating to your space.": "Edit settings relating to your space.",
|
||||
"Saving...": "Saving...",
|
||||
"Save Changes": "Save Changes",
|
||||
"Leave Space": "Leave Space",
|
||||
"Failed to update the visibility of this space": "Failed to update the visibility of this space",
|
||||
"Failed to update the guest access of this space": "Failed to update the guest access of this space",
|
||||
"Failed to update the history visibility of this space": "Failed to update the history visibility of this space",
|
||||
"Hide advanced": "Hide advanced",
|
||||
"Enable guest access": "Enable guest access",
|
||||
"Guests can join a space without having an account.": "Guests can join a space without having an account.",
|
||||
"This may be useful for public spaces.": "This may be useful for public spaces.",
|
||||
"Show advanced": "Show advanced",
|
||||
"Visibility": "Visibility",
|
||||
"Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.",
|
||||
"anyone with the link can view and join": "anyone with the link can view and join",
|
||||
"Invite only": "Invite only",
|
||||
"only invited people can view and join": "only invited people can view and join",
|
||||
"Preview Space": "Preview Space",
|
||||
"Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
|
||||
"Recommended for public spaces.": "Recommended for public spaces.",
|
||||
"Settings": "Settings",
|
||||
"Leave space": "Leave space",
|
||||
"Create new room": "Create new room",
|
||||
|
@ -1037,6 +1067,8 @@
|
|||
"Manage & explore rooms": "Manage & explore rooms",
|
||||
"Explore rooms": "Explore rooms",
|
||||
"Space options": "Space options",
|
||||
"Expand": "Expand",
|
||||
"Collapse": "Collapse",
|
||||
"Remove": "Remove",
|
||||
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
|
||||
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
|
||||
|
@ -1223,8 +1255,6 @@
|
|||
"Custom theme URL": "Custom theme URL",
|
||||
"Add theme": "Add theme",
|
||||
"Theme": "Theme",
|
||||
"Hide advanced": "Hide advanced",
|
||||
"Show advanced": "Show advanced",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||
"Customise your appearance": "Customise your appearance",
|
||||
|
@ -1245,7 +1275,6 @@
|
|||
"Deactivate Account": "Deactivate Account",
|
||||
"Deactivate account": "Deactivate account",
|
||||
"Discovery": "Discovery",
|
||||
"General": "General",
|
||||
"Legal": "Legal",
|
||||
"Credits": "Credits",
|
||||
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
|
||||
|
@ -1351,6 +1380,7 @@
|
|||
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
|
||||
"this room": "this room",
|
||||
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
|
||||
"Space information": "Space information",
|
||||
"Room information": "Room information",
|
||||
"Internal room ID:": "Internal room ID:",
|
||||
"Room version": "Room version",
|
||||
|
@ -1676,14 +1706,18 @@
|
|||
"Error removing address": "Error removing address",
|
||||
"Main address": "Main address",
|
||||
"not specified": "not specified",
|
||||
"This space has no local addresses": "This space has no local addresses",
|
||||
"This room has no local addresses": "This room has no local addresses",
|
||||
"Local address": "Local address",
|
||||
"Published Addresses": "Published Addresses",
|
||||
"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.": "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.",
|
||||
"Published addresses can be used by anyone on any server to join your space.": "Published addresses can be used by anyone on any server to join your space.",
|
||||
"Published addresses can be used by anyone on any server to join your room.": "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.": "To publish an address, it needs to be set as a local address first.",
|
||||
"Other published addresses:": "Other published addresses:",
|
||||
"No other published addresses yet, add one below": "No other published addresses yet, add one below",
|
||||
"New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)",
|
||||
"Local Addresses": "Local Addresses",
|
||||
"Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)",
|
||||
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)",
|
||||
"Show more": "Show more",
|
||||
"Error updating flair": "Error updating flair",
|
||||
|
@ -1932,6 +1966,7 @@
|
|||
"Error loading Widget": "Error loading Widget",
|
||||
"Error - Mixed content": "Error - Mixed content",
|
||||
"Popout widget": "Popout widget",
|
||||
"Message search initialisation failed, check <a>your settings</a> for more information": "Message search initialisation failed, check <a>your settings</a> for more information",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
|
||||
|
@ -2019,7 +2054,7 @@
|
|||
"Room address": "Room address",
|
||||
"e.g. my-room": "e.g. my-room",
|
||||
"Some characters not allowed": "Some characters not allowed",
|
||||
"Please provide a room address": "Please provide a room address",
|
||||
"Please provide an address": "Please provide an address",
|
||||
"This address is available to use": "This address is available to use",
|
||||
"This address is already in use": "This address is already in use",
|
||||
"Server Options": "Server Options",
|
||||
|
@ -2029,7 +2064,6 @@
|
|||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
"And %(count)s more...|other": "And %(count)s more...",
|
||||
"Home": "Home",
|
||||
"Enter a server name": "Enter a server name",
|
||||
"Looks good": "Looks good",
|
||||
"You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list",
|
||||
|
@ -2318,9 +2352,23 @@
|
|||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Please fill why you're reporting.": "Please fill why you're reporting.",
|
||||
"What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.",
|
||||
"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.\nThis will be reported to the room moderators.": "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.\nThis will be reported to the room moderators.",
|
||||
"This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.",
|
||||
"This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.",
|
||||
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.",
|
||||
"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.": "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.",
|
||||
"Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.",
|
||||
"Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.",
|
||||
"Report Content": "Report Content",
|
||||
"Disagree": "Disagree",
|
||||
"Toxic Behaviour": "Toxic Behaviour",
|
||||
"Illegal Content": "Illegal Content",
|
||||
"Spam or propaganda": "Spam or propaganda",
|
||||
"Report the entire room": "Report the entire room",
|
||||
"Send report": "Send report",
|
||||
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
|
||||
"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.": "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.",
|
||||
"Send report": "Send report",
|
||||
"Room Settings - %(roomName)s": "Room Settings - %(roomName)s",
|
||||
"Failed to upgrade room": "Failed to upgrade room",
|
||||
"The room upgrade could not be completed": "The room upgrade could not be completed",
|
||||
|
@ -2383,14 +2431,8 @@
|
|||
"Share Room Message": "Share Room Message",
|
||||
"Link to selected message": "Link to selected message",
|
||||
"Command Help": "Command Help",
|
||||
"Failed to save space settings.": "Failed to save space settings.",
|
||||
"Space settings": "Space settings",
|
||||
"Edit settings relating to your space.": "Edit settings relating to your space.",
|
||||
"Make this space private": "Make this space private",
|
||||
"Leave Space": "Leave Space",
|
||||
"View dev tools": "View dev tools",
|
||||
"Saving...": "Saving...",
|
||||
"Save Changes": "Save Changes",
|
||||
"Settings - %(spaceName)s": "Settings - %(spaceName)s",
|
||||
"To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
|
||||
"Missing session data": "Missing session data",
|
||||
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
|
||||
|
@ -2487,11 +2529,12 @@
|
|||
"Share Message": "Share Message",
|
||||
"Source URL": "Source URL",
|
||||
"Collapse Reply Thread": "Collapse Reply Thread",
|
||||
"Report Content": "Report Content",
|
||||
"Clear status": "Clear status",
|
||||
"Update status": "Update status",
|
||||
"Set status": "Set status",
|
||||
"Set a new status...": "Set a new status...",
|
||||
"Move up": "Move up",
|
||||
"Move down": "Move down",
|
||||
"View Community": "View Community",
|
||||
"Unable to start audio streaming.": "Unable to start audio streaming.",
|
||||
"Failed to start livestream": "Failed to start livestream",
|
||||
|
@ -2638,7 +2681,7 @@
|
|||
"%(count)s messages deleted.|one": "%(count)s message deleted.",
|
||||
"Your Communities": "Your Communities",
|
||||
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
|
||||
"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 filter panel at any time to see only the rooms and people associated with that community.": "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 filter panel at any time to see only the rooms and people associated with that community.",
|
||||
"You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
|
||||
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
||||
"Create a new community": "Create a new community",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
||||
|
@ -2993,5 +3036,6 @@
|
|||
"Esc": "Esc",
|
||||
"Enter": "Enter",
|
||||
"Space": "Space",
|
||||
"End": "End"
|
||||
"End": "End",
|
||||
"[number]": "[number]"
|
||||
}
|
||||
|
|
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