mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +03:00
Merge branch 'develop' into gsouquet-scroll-to-live-reset-hash
This commit is contained in:
commit
bc50028f70
96 changed files with 4936 additions and 1483 deletions
18
.eslintrc.js
18
.eslintrc.js
|
@ -30,6 +30,24 @@ module.exports = {
|
|||
|
||||
"quotes": "off",
|
||||
"no-extra-boolean-cast": "off",
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
...buildRestrictedPropertiesOptions(
|
||||
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||
"Use UIStore to access window dimensions instead",
|
||||
),
|
||||
],
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
function buildRestrictedPropertiesOptions(properties, message) {
|
||||
return properties.map(prop => {
|
||||
const [object, property] = prop.split(".");
|
||||
return {
|
||||
object,
|
||||
property,
|
||||
message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
113
CHANGELOG.md
113
CHANGELOG.md
|
@ -1,3 +1,116 @@
|
|||
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)
|
||||
|
||||
* Upgrade to JS SDK 11.1.0
|
||||
* [Release] Bump libolm version
|
||||
[\#6087](https://github.com/matrix-org/matrix-react-sdk/pull/6087)
|
||||
|
||||
Changes in [3.22.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0-rc.1) (2021-05-19)
|
||||
===============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0...v3.22.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 11.1.0-rc.1
|
||||
* Translations update from Weblate
|
||||
[\#6068](https://github.com/matrix-org/matrix-react-sdk/pull/6068)
|
||||
* Show DMs in space for invited members too, to match Android impl
|
||||
[\#6062](https://github.com/matrix-org/matrix-react-sdk/pull/6062)
|
||||
* Support filtering by alias in add existing to space dialog
|
||||
[\#6057](https://github.com/matrix-org/matrix-react-sdk/pull/6057)
|
||||
* Fix issue when a room without a name or alias is marked as suggested
|
||||
[\#6064](https://github.com/matrix-org/matrix-react-sdk/pull/6064)
|
||||
* Fix space room hierarchy not updating when removing a room
|
||||
[\#6055](https://github.com/matrix-org/matrix-react-sdk/pull/6055)
|
||||
* Revert "Try putting room list handling behind a lock"
|
||||
[\#6060](https://github.com/matrix-org/matrix-react-sdk/pull/6060)
|
||||
* Stop assuming encrypted messages are decrypted ahead of time
|
||||
[\#6052](https://github.com/matrix-org/matrix-react-sdk/pull/6052)
|
||||
* Add error detail when languges fail to load
|
||||
[\#6059](https://github.com/matrix-org/matrix-react-sdk/pull/6059)
|
||||
* Add space invaders chat effect
|
||||
[\#6053](https://github.com/matrix-org/matrix-react-sdk/pull/6053)
|
||||
* Create SpaceProvider and hide Spaces from the RoomProvider autocompleter
|
||||
[\#6051](https://github.com/matrix-org/matrix-react-sdk/pull/6051)
|
||||
* Don't mark a room as unread when redacted event is present
|
||||
[\#6049](https://github.com/matrix-org/matrix-react-sdk/pull/6049)
|
||||
* Add support for MSC2873: Client information for Widgets
|
||||
[\#6023](https://github.com/matrix-org/matrix-react-sdk/pull/6023)
|
||||
* Support UI for MSC2762: Widgets reading events from rooms
|
||||
[\#5960](https://github.com/matrix-org/matrix-react-sdk/pull/5960)
|
||||
* Fix crash on opening notification panel
|
||||
[\#6047](https://github.com/matrix-org/matrix-react-sdk/pull/6047)
|
||||
* Remove custom LoggedInView::shouldComponentUpdate logic
|
||||
[\#6046](https://github.com/matrix-org/matrix-react-sdk/pull/6046)
|
||||
* Fix edge cases with the new add reactions prompt button
|
||||
[\#6045](https://github.com/matrix-org/matrix-react-sdk/pull/6045)
|
||||
* Add ids to homeserver and passphrase fields
|
||||
[\#6043](https://github.com/matrix-org/matrix-react-sdk/pull/6043)
|
||||
* Update space order field validity requirements to match msc update
|
||||
[\#6042](https://github.com/matrix-org/matrix-react-sdk/pull/6042)
|
||||
* Try putting room list handling behind a lock
|
||||
[\#6024](https://github.com/matrix-org/matrix-react-sdk/pull/6024)
|
||||
* Improve progress bar progression for smaller voice messages
|
||||
[\#6035](https://github.com/matrix-org/matrix-react-sdk/pull/6035)
|
||||
* Fix share space edge case where space is public but not invitable
|
||||
[\#6039](https://github.com/matrix-org/matrix-react-sdk/pull/6039)
|
||||
* Add missing 'rel' to image view download button
|
||||
[\#6033](https://github.com/matrix-org/matrix-react-sdk/pull/6033)
|
||||
* Improve visible waveform for voice messages
|
||||
[\#6034](https://github.com/matrix-org/matrix-react-sdk/pull/6034)
|
||||
* Fix roving tab index intercepting home/end in space create menu
|
||||
[\#6040](https://github.com/matrix-org/matrix-react-sdk/pull/6040)
|
||||
* Decorate room avatars with publicity in add existing to space flow
|
||||
[\#6030](https://github.com/matrix-org/matrix-react-sdk/pull/6030)
|
||||
* Improve Spaces "Just Me" wizard
|
||||
[\#6025](https://github.com/matrix-org/matrix-react-sdk/pull/6025)
|
||||
* Increase hover feedback on room sub list buttons
|
||||
[\#6037](https://github.com/matrix-org/matrix-react-sdk/pull/6037)
|
||||
* Show alternative button during space creation wizard if no rooms
|
||||
[\#6029](https://github.com/matrix-org/matrix-react-sdk/pull/6029)
|
||||
* Swap rotation buttons in the image viewer
|
||||
[\#6032](https://github.com/matrix-org/matrix-react-sdk/pull/6032)
|
||||
* Typo: initilisation -> initialisation
|
||||
[\#5915](https://github.com/matrix-org/matrix-react-sdk/pull/5915)
|
||||
* Save edited state of a message when switching rooms
|
||||
[\#6001](https://github.com/matrix-org/matrix-react-sdk/pull/6001)
|
||||
* Fix shield icon in Untrusted Device Dialog
|
||||
[\#6022](https://github.com/matrix-org/matrix-react-sdk/pull/6022)
|
||||
* Do not eagerly decrypt breadcrumb rooms
|
||||
[\#6028](https://github.com/matrix-org/matrix-react-sdk/pull/6028)
|
||||
* Update spaces.png
|
||||
[\#6031](https://github.com/matrix-org/matrix-react-sdk/pull/6031)
|
||||
* Encourage more diverse reactions to content
|
||||
[\#6027](https://github.com/matrix-org/matrix-react-sdk/pull/6027)
|
||||
* Wrap decodeURIComponent in try-catch to protect against malformed URIs
|
||||
[\#6026](https://github.com/matrix-org/matrix-react-sdk/pull/6026)
|
||||
* Iterate beta feedback dialog
|
||||
[\#6021](https://github.com/matrix-org/matrix-react-sdk/pull/6021)
|
||||
* Disable space fields whilst their form is busy
|
||||
[\#6020](https://github.com/matrix-org/matrix-react-sdk/pull/6020)
|
||||
* Add missing space on beta feedback dialog
|
||||
[\#6018](https://github.com/matrix-org/matrix-react-sdk/pull/6018)
|
||||
* Fix colours used for the back button in space create menu
|
||||
[\#6017](https://github.com/matrix-org/matrix-react-sdk/pull/6017)
|
||||
* Prioritise and reduce the amount of events decrypted on application startup
|
||||
[\#5980](https://github.com/matrix-org/matrix-react-sdk/pull/5980)
|
||||
* Linkify topics in space room directory results
|
||||
[\#6015](https://github.com/matrix-org/matrix-react-sdk/pull/6015)
|
||||
* Persistent space collapsed states
|
||||
[\#5972](https://github.com/matrix-org/matrix-react-sdk/pull/5972)
|
||||
* Catch another instance of unlabeled avatars.
|
||||
[\#6010](https://github.com/matrix-org/matrix-react-sdk/pull/6010)
|
||||
* Rescale and smooth voice message playback waveform to better match
|
||||
expectation
|
||||
[\#5996](https://github.com/matrix-org/matrix-react-sdk/pull/5996)
|
||||
* Scale voice message clock with user's font size
|
||||
[\#5993](https://github.com/matrix-org/matrix-react-sdk/pull/5993)
|
||||
* Remove "in development" flag from voice messages
|
||||
[\#5995](https://github.com/matrix-org/matrix-react-sdk/pull/5995)
|
||||
* Support voice messages on Safari
|
||||
[\#5989](https://github.com/matrix-org/matrix-react-sdk/pull/5989)
|
||||
* Translations update from Weblate
|
||||
[\#6011](https://github.com/matrix-org/matrix-react-sdk/pull/6011)
|
||||
|
||||
Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.21.0",
|
||||
"version": "3.22.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -121,6 +121,7 @@
|
|||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.12",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@peculiar/webcrypto": "^1.1.4",
|
||||
"@sinonjs/fake-timers": "^7.0.2",
|
||||
"@types/classnames": "^2.2.11",
|
||||
|
@ -161,7 +162,6 @@
|
|||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.2.2",
|
||||
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"stylelint": "^13.9.0",
|
||||
|
|
|
@ -45,6 +45,8 @@ html {
|
|||
N.B. Breaks things when we have legitimate horizontal overscroll */
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
// Stop similar overscroll bounce in Firefox Nightly for macOS
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
@ -328,6 +328,7 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
font-size: $font-15px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
> hr {
|
||||
|
|
|
@ -20,11 +20,12 @@ limitations under the License.
|
|||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 24px;
|
||||
height: 32px;
|
||||
line-height: $font-24px;
|
||||
border-radius: 4px;
|
||||
background: $message-action-bar-bg-color;
|
||||
top: -26px;
|
||||
border-radius: 8px;
|
||||
background: $primary-bg-color;
|
||||
border: 1px solid $input-border-color;
|
||||
top: -32px;
|
||||
right: 8px;
|
||||
user-select: none;
|
||||
// Ensure the action bar appears above over things, like the read marker.
|
||||
|
@ -51,31 +52,19 @@ limitations under the License.
|
|||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 1px solid $message-action-bar-border-color;
|
||||
margin-left: -1px;
|
||||
margin: 2px;
|
||||
|
||||
&:hover {
|
||||
border-color: $message-action-bar-hover-border-color;
|
||||
background: $roomlist-button-bg-color;
|
||||
border-radius: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mx_MessageActionBar_maskButton {
|
||||
width: 27px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_maskButton::after {
|
||||
|
@ -88,7 +77,11 @@ limitations under the License.
|
|||
mask-size: 18px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: $message-action-bar-fg-color;
|
||||
background-color: $secondary-fg-color;
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_maskButton:hover::after {
|
||||
background-color: $primary-fg-color;
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_reactButton::after {
|
||||
|
|
|
@ -104,7 +104,7 @@ $left-gutter: 64px;
|
|||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
position: relative;
|
||||
padding-left: $left-gutter;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_RoomView_timeline_rr_enabled,
|
||||
|
@ -280,6 +280,7 @@ $left-gutter: 64px;
|
|||
height: $font-14px;
|
||||
width: $font-14px;
|
||||
|
||||
will-change: left, top;
|
||||
transition:
|
||||
left var(--transition-short) ease-out,
|
||||
top var(--transition-standard) ease-out;
|
||||
|
|
|
@ -61,8 +61,8 @@ limitations under the License.
|
|||
&.mx_RoomSublist_headerContainer_sticky {
|
||||
position: fixed;
|
||||
height: 32px; // to match the header container
|
||||
// width set by JS
|
||||
width: calc(100% - 22px);
|
||||
// width set by JS because of a compat issue between Firefox and Chrome
|
||||
width: calc(100% - 15px);
|
||||
}
|
||||
|
||||
// We don't have a top style because the top is dependent on the room list header's
|
||||
|
|
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -43,6 +43,7 @@ import TypingStore from "../stores/TypingStore";
|
|||
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
||||
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
||||
import PerformanceMonitor from "../performance";
|
||||
import UIStore from "../stores/UIStore";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -82,6 +83,7 @@ declare global {
|
|||
mxEventIndexPeg: EventIndexPeg;
|
||||
mxPerformanceMonitor: PerformanceMonitor;
|
||||
mxPerformanceEntryNames: any;
|
||||
mxUIStore: UIStore;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
|
|
|
@ -462,6 +462,9 @@ export default class CallHandler extends EventEmitter {
|
|||
if (call.hangupReason === CallErrorCode.UserHangup) {
|
||||
title = _t("Call Declined");
|
||||
description = _t("The other party declined the call.");
|
||||
} else if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||
title = _t("User Busy");
|
||||
description = _t("The user you called is busy.");
|
||||
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
||||
title = _t("Call Failed");
|
||||
// XXX: full stop appended as some relic here, but these
|
||||
|
@ -799,7 +802,10 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||
if (this.getCallForRoom(mappedRoomId)) {
|
||||
// ignore multiple incoming calls to the same room
|
||||
console.log(
|
||||
"Got incoming call for room " + mappedRoomId +
|
||||
" but there's already a call for this room: ignoring",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import SdkConfig from './SdkConfig';
|
|||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import {sleep} from "./utils/promise";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
|
||||
// polyfill textencoder if necessary
|
||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||
|
@ -265,7 +266,7 @@ interface ICreateRoomEvent extends IEvent {
|
|||
}
|
||||
|
||||
interface IJoinRoomEvent extends IEvent {
|
||||
key: "join_room";
|
||||
key: Action.JoinRoom;
|
||||
dur: number; // how long it took to join (until remote echo)
|
||||
segmentation: {
|
||||
room_id: string; // hashed
|
||||
|
@ -684,7 +685,9 @@ export default class CountlyAnalytics {
|
|||
}
|
||||
|
||||
private getOrientation = (): Orientation => {
|
||||
return window.innerWidth > window.innerHeight ? Orientation.Landscape : Orientation.Portrait;
|
||||
return window.matchMedia("(orientation: landscape)").matches
|
||||
? Orientation.Landscape
|
||||
: Orientation.Portrait
|
||||
};
|
||||
|
||||
private reportOrientation = () => {
|
||||
|
@ -813,7 +816,9 @@ export default class CountlyAnalytics {
|
|||
window.addEventListener("mousemove", this.onUserActivity);
|
||||
window.addEventListener("click", this.onUserActivity);
|
||||
window.addEventListener("keydown", this.onUserActivity);
|
||||
window.addEventListener("scroll", this.onUserActivity);
|
||||
// Using the passive option to not block the main thread
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||
window.addEventListener("scroll", this.onUserActivity, { passive: true });
|
||||
|
||||
this.activityIntervalId = setInterval(() => {
|
||||
this.inactivityCounter++;
|
||||
|
@ -858,7 +863,7 @@ export default class CountlyAnalytics {
|
|||
}
|
||||
|
||||
public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) {
|
||||
this.track<IJoinRoomEvent>("join_room", { type }, roomId, {
|
||||
this.track<IJoinRoomEvent>(Action.JoinRoom, { type }, roomId, {
|
||||
dur: CountlyAnalytics.getTimestamp() - startTime,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import MultiInviter from './utils/MultiInviter';
|
|||
import { _t } from './languageHandler';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import GroupStore from './stores/GroupStore';
|
||||
import {allSettled} from "./utils/promise";
|
||||
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
||||
|
||||
export function showGroupInviteDialog(groupId) {
|
||||
|
@ -120,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) {
|
|||
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
const errorList = [];
|
||||
return allSettled(addrs.map((addr) => {
|
||||
return Promise.allSettled(addrs.map((addr) => {
|
||||
return GroupStore
|
||||
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
|
||||
.catch(() => { errorList.push(addr.address); })
|
||||
|
|
15
src/Login.ts
15
src/Login.ts
|
@ -31,12 +31,12 @@ interface IPasswordFlow {
|
|||
}
|
||||
|
||||
export enum IdentityProviderBrand {
|
||||
Gitlab = "org.matrix.gitlab",
|
||||
Github = "org.matrix.github",
|
||||
Apple = "org.matrix.apple",
|
||||
Google = "org.matrix.google",
|
||||
Facebook = "org.matrix.facebook",
|
||||
Twitter = "org.matrix.twitter",
|
||||
Gitlab = "gitlab",
|
||||
Github = "github",
|
||||
Apple = "apple",
|
||||
Google = "google",
|
||||
Facebook = "facebook",
|
||||
Twitter = "twitter",
|
||||
}
|
||||
|
||||
export interface IIdentityProvider {
|
||||
|
@ -48,7 +48,8 @@ export interface IIdentityProvider {
|
|||
|
||||
export interface ISSOFlow {
|
||||
type: "m.login.sso" | "m.login.cas";
|
||||
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
|
||||
// eslint-disable-next-line camelcase
|
||||
identity_providers: IIdentityProvider[];
|
||||
}
|
||||
|
||||
export type LoginFlow = ISSOFlow | IPasswordFlow;
|
||||
|
|
14
src/Terms.ts
14
src/Terms.ts
|
@ -36,14 +36,18 @@ export class Service {
|
|||
}
|
||||
}
|
||||
|
||||
interface Policy {
|
||||
export interface LocalisedPolicy {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
// @ts-ignore: No great way to express indexed types together with other keys
|
||||
version: string;
|
||||
[lang: string]: {
|
||||
url: string;
|
||||
};
|
||||
[lang: string]: LocalisedPolicy;
|
||||
}
|
||||
type Policies = {
|
||||
|
||||
export type Policies = {
|
||||
[policy: string]: Policy,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector 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 from "react";
|
||||
|
||||
export default class AutoHideScrollbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._collectContainerRef = this._collectContainerRef.bind(this);
|
||||
}
|
||||
|
||||
_collectContainerRef(ref) {
|
||||
if (ref && !this.containerRef) {
|
||||
this.containerRef = ref;
|
||||
}
|
||||
if (this.props.wrappedRef) {
|
||||
this.props.wrappedRef(ref);
|
||||
}
|
||||
}
|
||||
|
||||
getScrollTop() {
|
||||
return this.containerRef.scrollTop;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div
|
||||
ref={this._collectContainerRef}
|
||||
style={this.props.style}
|
||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||
onScroll={this.props.onScroll}
|
||||
onWheel={this.props.onWheel}
|
||||
tabIndex={this.props.tabIndex}
|
||||
>
|
||||
{ this.props.children }
|
||||
</div>);
|
||||
}
|
||||
}
|
65
src/components/structures/AutoHideScrollbar.tsx
Normal file
65
src/components/structures/AutoHideScrollbar.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2018 New Vector 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 from "react";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
onScroll?: () => void;
|
||||
onWheel?: () => void;
|
||||
style?: React.CSSProperties
|
||||
tabIndex?: number,
|
||||
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||
}
|
||||
|
||||
export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||
private containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.containerRef.current && this.props.onScroll) {
|
||||
// Using the passive option to not block the main thread
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
|
||||
}
|
||||
|
||||
if (this.props.wrappedRef) {
|
||||
this.props.wrappedRef(this.containerRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.containerRef.current && this.props.onScroll) {
|
||||
this.containerRef.current.removeEventListener("scroll", this.props.onScroll);
|
||||
}
|
||||
}
|
||||
|
||||
public getScrollTop(): number {
|
||||
return this.containerRef.current.scrollTop;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (<div
|
||||
ref={this.containerRef}
|
||||
style={this.props.style}
|
||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||
onWheel={this.props.onWheel}
|
||||
tabIndex={this.props.tabIndex}
|
||||
>
|
||||
{ this.props.children }
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import classNames from "classnames";
|
|||
import {Key} from "../../Keyboard";
|
||||
import {Writeable} from "../../@types/common";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -410,12 +411,12 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
|||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
if (buttonBottom < UIStore.instance.windowHeight / 2) {
|
||||
menuOptions.top = buttonBottom + vPadding;
|
||||
} else {
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||
}
|
||||
|
||||
return menuOptions;
|
||||
|
@ -430,12 +431,12 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
|
|||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
if (buttonBottom < UIStore.instance.windowHeight / 2) {
|
||||
menuOptions.top = buttonBottom + vPadding;
|
||||
} else {
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||
}
|
||||
|
||||
return menuOptions;
|
||||
|
@ -451,7 +452,7 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa
|
|||
// Align the left edge of the menu to the left edge of the button
|
||||
menuOptions.left = buttonLeft;
|
||||
// Align the menu vertically above the menu
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||
|
||||
return menuOptions;
|
||||
};
|
||||
|
|
|
@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore';
|
|||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||
import {Group} from "matrix-js-sdk/src/models/group";
|
||||
import {allSettled, sleep} from "../../utils/promise";
|
||||
import {sleep} from "../../utils/promise";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
|
@ -99,7 +99,7 @@ class CategoryRoomList extends React.Component {
|
|||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
const errorList = [];
|
||||
allSettled(addrs.map((addr) => {
|
||||
Promise.allSettled(addrs.map((addr) => {
|
||||
return GroupStore
|
||||
.addRoomToGroupSummary(this.props.groupId, addr.address)
|
||||
.catch(() => { errorList.push(addr.address); });
|
||||
|
@ -274,7 +274,7 @@ class RoleUserList extends React.Component {
|
|||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
const errorList = [];
|
||||
allSettled(addrs.map((addr) => {
|
||||
Promise.allSettled(addrs.map((addr) => {
|
||||
return GroupStore
|
||||
.addUserToGroupSummary(addr.address)
|
||||
.catch(() => { errorList.push(addr.address); });
|
||||
|
|
|
@ -59,7 +59,9 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
_collectScroller(scroller) {
|
||||
if (scroller && !this._scrollElement) {
|
||||
this._scrollElement = scroller;
|
||||
this._scrollElement.addEventListener("scroll", this.checkOverflow);
|
||||
// Using the passive option to not block the main thread
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
||||
this.checkOverflow();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import {replaceableComponent} from "../../utils/replaceableComponent";
|
|||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -66,6 +67,7 @@ const cssClasses = [
|
|||
|
||||
@replaceableComponent("structures.LeftPanel")
|
||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||
private groupFilterPanelWatcherRef: string;
|
||||
private bgImageWatcherRef: string;
|
||||
|
@ -90,10 +92,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
||||
});
|
||||
}
|
||||
|
||||
// We watch the middle panel because we don't actually get resized, the middle panel does.
|
||||
// We listen to the noisy channel to avoid choppy reaction times.
|
||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
||||
public componentDidMount() {
|
||||
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
||||
// Using the passive option to not block the main thread
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||
this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -103,7 +109,15 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
||||
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
||||
this.listContainerRef.current?.removeEventListener("scroll", this.onScroll);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
if (prevState.activeSpace !== this.state.activeSpace) {
|
||||
this.refreshStickyHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
private updateActiveSpace = (activeSpace: Room) => {
|
||||
|
@ -114,6 +128,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
|
||||
private refreshStickyHeaders = () => {
|
||||
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
||||
this.handleStickyHeaders(this.listContainerRef.current);
|
||||
}
|
||||
|
||||
private onBreadcrumbsUpdate = () => {
|
||||
const newVal = BreadcrumbsStore.instance.visible;
|
||||
if (newVal !== this.state.showBreadcrumbs) {
|
||||
|
@ -156,9 +175,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
|
||||
|
||||
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
||||
const headerStickyWidth = list.clientWidth - headerRightMargin;
|
||||
|
||||
// We track which styles we want on a target before making the changes to avoid
|
||||
// excessive layout updates.
|
||||
const targetStyles = new Map<HTMLDivElement, {
|
||||
|
@ -228,7 +244,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
|
||||
}
|
||||
|
||||
const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight);
|
||||
const offset = UIStore.instance.windowHeight -
|
||||
(list.parentElement.offsetTop + list.parentElement.offsetHeight);
|
||||
const newBottom = `${offset}px`;
|
||||
if (header.style.bottom !== newBottom) {
|
||||
header.style.bottom = newBottom;
|
||||
|
@ -247,14 +264,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
||||
}
|
||||
|
||||
const newWidth = `${headerStickyWidth}px`;
|
||||
if (header.style.width !== newWidth) {
|
||||
header.style.width = newWidth;
|
||||
const listDimensions = UIStore.instance.getElementDimensions("ListContainer");
|
||||
if (listDimensions) {
|
||||
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
||||
const headerStickyWidth = listDimensions.width - headerRightMargin;
|
||||
const newWidth = `${headerStickyWidth}px`;
|
||||
if (header.style.width !== newWidth) {
|
||||
header.style.width = newWidth;
|
||||
}
|
||||
}
|
||||
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
||||
}
|
||||
|
||||
if (header.style.width) {
|
||||
header.style.removeProperty('width');
|
||||
}
|
||||
|
@ -276,16 +299,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
private onScroll = (ev: Event) => {
|
||||
const list = ev.target as HTMLDivElement;
|
||||
this.handleStickyHeaders(list);
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
||||
this.handleStickyHeaders(this.listContainerRef.current);
|
||||
};
|
||||
|
||||
private onFocus = (ev: React.FocusEvent) => {
|
||||
this.focusedElement = ev.target;
|
||||
};
|
||||
|
@ -420,8 +438,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.onResize}
|
||||
activeSpace={this.state.activeSpace}
|
||||
onListCollapse={this.refreshStickyHeaders}
|
||||
/>;
|
||||
|
||||
const containerClasses = classNames({
|
||||
|
@ -435,17 +453,16 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={containerClasses} ref={this.ref}>
|
||||
{leftLeftPanel}
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
{this.renderHeader()}
|
||||
{this.renderSearchExplore()}
|
||||
{this.renderBreadcrumbs()}
|
||||
<RoomListNumResults />
|
||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
<div
|
||||
className={roomListClasses}
|
||||
onScroll={this.onScroll}
|
||||
ref={this.listContainerRef}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
|
@ -454,7 +471,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
{roomList}
|
||||
</div>
|
||||
</div>
|
||||
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
|
||||
{ !this.props.isMinimized && <LeftPanelWidget /> }
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useEffect, useMemo} from "react";
|
||||
import React, {useContext, useMemo} from "react";
|
||||
import {Resizable} from "re-resizable";
|
||||
import classNames from "classnames";
|
||||
|
||||
|
@ -27,16 +27,13 @@ import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils";
|
|||
import {useAccountData} from "../../hooks/useAccountData";
|
||||
import AppTile from "../views/elements/AppTile";
|
||||
import {useSettingValue} from "../../hooks/useSettings";
|
||||
|
||||
interface IProps {
|
||||
onResize(): void;
|
||||
}
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
const MIN_HEIGHT = 100;
|
||||
const MAX_HEIGHT = 500; // or 50% of the window height
|
||||
const INITIAL_HEIGHT = 280;
|
||||
|
||||
const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
||||
const LeftPanelWidget: React.FC = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
|
||||
|
@ -56,7 +53,6 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
|||
|
||||
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
||||
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
||||
useEffect(onResize, [expanded, onResize]);
|
||||
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
@ -68,8 +64,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
|||
content = <Resizable
|
||||
size={{height} as any}
|
||||
minHeight={MIN_HEIGHT}
|
||||
maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)}
|
||||
onResize={onResize}
|
||||
maxHeight={Math.min(UIStore.instance.windowHeight / 2, MAX_HEIGHT)}
|
||||
onResizeStop={(e, dir, ref, d) => {
|
||||
setHeight(height + d.height);
|
||||
}}
|
||||
|
|
|
@ -87,6 +87,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
|||
import SecurityCustomisations from "../../customisations/Security";
|
||||
|
||||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -225,13 +226,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
firstSyncPromise: IDeferred<void>;
|
||||
|
||||
private screenAfterLogin?: IScreen;
|
||||
private windowWidth: number;
|
||||
private pageChanging: boolean;
|
||||
private tokenLogin?: boolean;
|
||||
private accountPassword?: string;
|
||||
private accountPasswordTimer?: NodeJS.Timeout;
|
||||
private focusComposer: boolean;
|
||||
private subTitleStatus: string;
|
||||
private prevWindowWidth: number;
|
||||
|
||||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||
private readonly dispatcherRef: any;
|
||||
|
@ -277,9 +278,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
this.windowWidth = 10000;
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
|
||||
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
|
||||
|
||||
this.pageChanging = false;
|
||||
|
||||
|
@ -436,7 +436,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
dis.unregister(this.dispatcherRef);
|
||||
this.themeWatcher.stop();
|
||||
this.fontWatcher.stop();
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
UIStore.destroy();
|
||||
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
||||
|
||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
|
@ -1820,18 +1820,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
handleResize = () => {
|
||||
const hideLhsThreshold = 1000;
|
||||
const showLhsThreshold = 1000;
|
||||
const LHS_THRESHOLD = 1000;
|
||||
const width = UIStore.instance.windowWidth;
|
||||
|
||||
if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||
dis.dispatch({ action: 'hide_left_panel' });
|
||||
}
|
||||
if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
||||
if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) {
|
||||
dis.dispatch({ action: 'show_left_panel' });
|
||||
}
|
||||
|
||||
if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) {
|
||||
dis.dispatch({ action: 'hide_left_panel' });
|
||||
}
|
||||
|
||||
this.prevWindowWidth = width;
|
||||
this.state.resizeNotifier.notifyWindowResized();
|
||||
this.windowWidth = window.innerWidth;
|
||||
};
|
||||
|
||||
private dispatchTimelineResize() {
|
||||
|
@ -2090,6 +2091,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername}
|
||||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -83,6 +83,7 @@ import { objectHasDiff } from "../../utils/objects";
|
|||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -1125,7 +1126,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
Promise.resolve().then(() => {
|
||||
const signUrl = this.props.threepidInvite?.signUrl;
|
||||
dis.dispatch({
|
||||
action: 'join_room',
|
||||
action: Action.JoinRoom,
|
||||
roomId: this.getRoomId(),
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
_type: "unknown", // TODO: instrumentation
|
||||
});
|
||||
|
@ -1598,7 +1600,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// a maxHeight on the underlying remote video tag.
|
||||
|
||||
// header + footer + status + give us at least 120px of scrollback at all times.
|
||||
let auxPanelMaxHeight = window.innerHeight -
|
||||
let auxPanelMaxHeight = UIStore.instance.windowHeight -
|
||||
(54 + // height of RoomHeader
|
||||
36 + // height of the status area
|
||||
51 + // minimum height of the message compmoser
|
||||
|
|
|
@ -902,13 +902,13 @@ export default class ScrollPanel extends React.Component {
|
|||
onScroll={this.onScroll}
|
||||
onWheel={this.props.onUserScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
{ this.props.fixedChildren }
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
{ this.props.fixedChildren }
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,15 +101,13 @@ const Tile: React.FC<ITileProps> = ({
|
|||
numChildRooms,
|
||||
children,
|
||||
}) => {
|
||||
const name = room.name || room.canonical_alias || room.aliases?.[0]
|
||||
const cli = MatrixClientPeg.get();
|
||||
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
||||
const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0]
|
||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cliRoom = cli.getRoom(room.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
const onPreviewClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -122,7 +120,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
}
|
||||
|
||||
let button;
|
||||
if (myMembership === "join") {
|
||||
if (joinedRoom) {
|
||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
||||
{ _t("View") }
|
||||
</AccessibleButton>;
|
||||
|
@ -146,17 +144,27 @@ const Tile: React.FC<ITileProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
let url: string;
|
||||
if (room.avatar_url) {
|
||||
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20);
|
||||
let avatar;
|
||||
if (joinedRoom) {
|
||||
avatar = <RoomAvatar room={joinedRoom} width={20} height={20} />;
|
||||
} else {
|
||||
avatar = <BaseAvatar
|
||||
name={name}
|
||||
idName={room.room_id}
|
||||
url={room.avatar_url ? mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20) : null}
|
||||
width={20}
|
||||
height={20}
|
||||
/>;
|
||||
}
|
||||
|
||||
let description = _t("%(count)s members", { count: room.num_joined_members });
|
||||
if (numChildRooms !== undefined) {
|
||||
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
||||
}
|
||||
if (room.topic) {
|
||||
description += " · " + room.topic;
|
||||
|
||||
const topic = joinedRoom?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic;
|
||||
if (topic) {
|
||||
description += " · " + topic;
|
||||
}
|
||||
|
||||
let suggestedSection;
|
||||
|
@ -167,7 +175,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
}
|
||||
|
||||
const content = <React.Fragment>
|
||||
<BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
|
||||
{ avatar }
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
||||
{ name }
|
||||
{ suggestedSection }
|
||||
|
@ -311,7 +319,7 @@ export const HierarchyLevel = ({
|
|||
key={roomId}
|
||||
room={rooms.get(roomId)}
|
||||
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
|
||||
.filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
|
||||
.filter(ev => rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length}
|
||||
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
||||
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
||||
onViewRoomClick={(autoJoin) => {
|
||||
|
@ -429,7 +437,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
|
||||
let content;
|
||||
if (roomsMap) {
|
||||
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
|
||||
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||
|
||||
let countsStr;
|
||||
|
|
|
@ -417,9 +417,13 @@ const SpaceLanding = ({ space }) => {
|
|||
{ inviteButton }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_topic">
|
||||
<RoomTopic room={space} />
|
||||
</div>
|
||||
<RoomTopic room={space}>
|
||||
{(topic, ref) => (
|
||||
<div className="mx_SpaceRoomView_landing_topic" ref={ref}>
|
||||
{ topic }
|
||||
</div>
|
||||
)}
|
||||
</RoomTopic>
|
||||
<SpaceFeedbackPrompt />
|
||||
<hr />
|
||||
|
||||
|
@ -437,7 +441,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
const [error, setError] = useState("");
|
||||
const numFields = 3;
|
||||
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
||||
// TODO vary default prefills for "Just Me" spaces
|
||||
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
||||
const name = "roomName" + i;
|
||||
|
|
|
@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
|
|||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import TooltipButton from "../views/elements/TooltipButton";
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
@ -68,6 +69,7 @@ interface IState {
|
|||
contextMenuPosition: PartialDOMRect;
|
||||
isDarkTheme: boolean;
|
||||
selectedSpace?: Room;
|
||||
pendingRoomJoin: Set<string>;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.UserMenu")
|
||||
|
@ -84,6 +86,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.state = {
|
||||
contextMenuPosition: null,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
pendingRoomJoin: new Set<string>(),
|
||||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
|
@ -103,6 +106,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -114,6 +118,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||
}
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
this.removePendingJoinRoom(room.roomId);
|
||||
}
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
|
@ -147,15 +156,39 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onAction = (ev: ActionPayload) => {
|
||||
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
||||
|
||||
if (this.state.contextMenuPosition) {
|
||||
this.setState({contextMenuPosition: null});
|
||||
} else {
|
||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||
switch (ev.action) {
|
||||
case Action.ToggleUserMenu:
|
||||
if (this.state.contextMenuPosition) {
|
||||
this.setState({contextMenuPosition: null});
|
||||
} else {
|
||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||
}
|
||||
break;
|
||||
case Action.JoinRoom:
|
||||
this.addPendingJoinRoom(ev.roomId);
|
||||
break;
|
||||
case Action.JoinRoomReady:
|
||||
case Action.JoinRoomError:
|
||||
this.removePendingJoinRoom(ev.roomId);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private addPendingJoinRoom(roomId: string): void {
|
||||
this.setState({
|
||||
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
|
||||
.add(roomId),
|
||||
});
|
||||
}
|
||||
|
||||
private removePendingJoinRoom(roomId: string): void {
|
||||
if (this.state.pendingRoomJoin.delete(roomId)) {
|
||||
this.setState({
|
||||
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -617,6 +650,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</span>
|
||||
{name}
|
||||
{this.state.pendingRoomJoin.size > 0 && (
|
||||
<InlineSpinner>
|
||||
<TooltipButton helpText={_t(
|
||||
"Currently joining %(count)s rooms",
|
||||
{ count: this.state.pendingRoomJoin.size },
|
||||
)} />
|
||||
</InlineSpinner>
|
||||
)}
|
||||
{dnd}
|
||||
{buttons}
|
||||
</div>
|
||||
|
|
|
@ -59,6 +59,7 @@ interface IProps {
|
|||
fallbackHsUrl?: string;
|
||||
defaultDeviceDisplayName?: string;
|
||||
fragmentAfterLogin?: string;
|
||||
defaultUsername?: string;
|
||||
|
||||
// Called when the user has logged in. Params:
|
||||
// - The object returned by the login API
|
||||
|
@ -119,7 +120,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
|
||||
flows: null,
|
||||
|
||||
username: "",
|
||||
username: props.defaultUsername? props.defaultUsername: '',
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
|
||||
|
|
|
@ -223,7 +223,8 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
this.setState({
|
||||
flows: e.data.flows,
|
||||
});
|
||||
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
|
||||
} else if (e.httpStatus === 403 || e.errcode === "M_FORBIDDEN") {
|
||||
// Check for 403 or M_FORBIDDEN, Synapse used to send 403 M_UNKNOWN but now sends 403 M_FORBIDDEN.
|
||||
// At this point registration is pretty much disabled, but before we do that let's
|
||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||
// the user off to the login page to figure their account out.
|
||||
|
@ -467,7 +468,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
let ssoSection;
|
||||
if (this.state.ssoFlow) {
|
||||
let continueWithSection;
|
||||
const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] || [];
|
||||
const providers = this.state.ssoFlow.identity_providers || [];
|
||||
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
|
||||
if (providers.length > 1) {
|
||||
// i18n: ssoButtons is a placeholder to help translators understand context
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -16,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import Spinner from "../elements/Spinner";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { LocalisedPolicy, Policies } from '../../../Terms';
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -74,36 +73,72 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
* focus: set the input focus appropriately in the form.
|
||||
*/
|
||||
|
||||
enum AuthType {
|
||||
Password = "m.login.password",
|
||||
Recaptcha = "m.login.recaptcha",
|
||||
Terms = "m.login.terms",
|
||||
Email = "m.login.email.identity",
|
||||
Msisdn = "m.login.msisdn",
|
||||
Sso = "m.login.sso",
|
||||
SsoUnstable = "org.matrix.login.sso",
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IAuthDict {
|
||||
type?: AuthType;
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user?: string;
|
||||
identifier?: any;
|
||||
password?: string;
|
||||
response?: string;
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds?: any;
|
||||
threepidCreds?: any;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export const DEFAULT_PHASE = 0;
|
||||
|
||||
@replaceableComponent("views.auth.PasswordAuthEntry")
|
||||
export class PasswordAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.password";
|
||||
interface IAuthEntryProps {
|
||||
matrixClient: MatrixClient;
|
||||
loginType: string;
|
||||
authSessionId: string;
|
||||
errorText?: string;
|
||||
// Is the auth logic currently waiting for something to happen?
|
||||
busy?: boolean;
|
||||
onPhaseChange: (phase: number) => void;
|
||||
submitAuthDict: (auth: IAuthDict) => void;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
// is the auth logic currently waiting for something to
|
||||
// happen?
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IPasswordAuthEntryState {
|
||||
password: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.PasswordAuthEntry")
|
||||
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Password;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
password: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
state = {
|
||||
password: "",
|
||||
};
|
||||
|
||||
_onSubmit = e => {
|
||||
private onSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (this.props.busy) return;
|
||||
|
||||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
type: AuthType.Password,
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user: this.props.matrixClient.credentials.userId,
|
||||
|
@ -115,7 +150,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onPasswordFieldChange = ev => {
|
||||
private onPasswordFieldChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.setState({
|
||||
password: ev.target.value,
|
||||
|
@ -123,7 +158,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const passwordBoxClass = classnames({
|
||||
const passwordBoxClass = classNames({
|
||||
"error": this.props.errorText,
|
||||
});
|
||||
|
||||
|
@ -155,7 +190,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
className={passwordBoxClass}
|
||||
type="password"
|
||||
|
@ -163,7 +198,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
label={_t('Password')}
|
||||
autoFocus={true}
|
||||
value={this.state.password}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
onChange={this.onPasswordFieldChange}
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
{ submitButtonOrSpinner }
|
||||
|
@ -175,26 +210,26 @@ export class PasswordAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
||||
export class RecaptchaAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.recaptcha";
|
||||
|
||||
static propTypes = {
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
/* eslint-disable camelcase */
|
||||
interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
|
||||
stageParams?: {
|
||||
public_key?: string;
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
||||
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
|
||||
static LOGIN_TYPE = AuthType.Recaptcha;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
_onCaptchaResponse = response => {
|
||||
private onCaptchaResponse = (response: string) => {
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
||||
this.props.submitAuthDict({
|
||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
||||
type: AuthType.Recaptcha,
|
||||
response: response,
|
||||
});
|
||||
};
|
||||
|
@ -230,7 +265,7 @@ export class RecaptchaAuthEntry extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<CaptchaForm sitePublicKey={sitePublicKey}
|
||||
onCaptchaResponse={this._onCaptchaResponse}
|
||||
onCaptchaResponse={this.onCaptchaResponse}
|
||||
/>
|
||||
{ errorSection }
|
||||
</div>
|
||||
|
@ -238,18 +273,28 @@ export class RecaptchaAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.TermsAuthEntry")
|
||||
export class TermsAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.terms";
|
||||
|
||||
static propTypes = {
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
showContinue: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
interface ITermsAuthEntryProps extends IAuthEntryProps {
|
||||
stageParams?: {
|
||||
policies?: Policies;
|
||||
};
|
||||
showContinue: boolean;
|
||||
}
|
||||
|
||||
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ITermsAuthEntryState {
|
||||
policies: LocalisedPolicyWithId[];
|
||||
toggledPolicies: {
|
||||
[policy: string]: boolean;
|
||||
};
|
||||
errorText?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.TermsAuthEntry")
|
||||
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Terms;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -294,8 +339,11 @@ export class TermsAuthEntry extends React.Component {
|
|||
|
||||
initToggles[policyId] = false;
|
||||
|
||||
langPolicy.id = policyId;
|
||||
pickedPolicies.push(langPolicy);
|
||||
pickedPolicies.push({
|
||||
id: policyId,
|
||||
name: langPolicy.name,
|
||||
url: langPolicy.url,
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
|
@ -311,11 +359,11 @@ export class TermsAuthEntry extends React.Component {
|
|||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
tryContinue = () => {
|
||||
this._trySubmit();
|
||||
public tryContinue = () => {
|
||||
this.trySubmit();
|
||||
};
|
||||
|
||||
_togglePolicy(policyId) {
|
||||
private togglePolicy(policyId: string) {
|
||||
const newToggles = {};
|
||||
for (const policy of this.state.policies) {
|
||||
let checked = this.state.toggledPolicies[policy.id];
|
||||
|
@ -326,7 +374,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
this.setState({"toggledPolicies": newToggles});
|
||||
}
|
||||
|
||||
_trySubmit = () => {
|
||||
private trySubmit = () => {
|
||||
let allChecked = true;
|
||||
for (const policy of this.state.policies) {
|
||||
const checked = this.state.toggledPolicies[policy.id];
|
||||
|
@ -334,7 +382,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
}
|
||||
|
||||
if (allChecked) {
|
||||
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
||||
this.props.submitAuthDict({type: AuthType.Terms});
|
||||
CountlyAnalytics.instance.track("onboarding_terms_complete");
|
||||
} else {
|
||||
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||
|
@ -356,7 +404,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
checkboxes.push(
|
||||
// XXX: replace with StyledCheckbox
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<input type="checkbox" onChange={() => this.togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
</label>,
|
||||
);
|
||||
|
@ -375,7 +423,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
if (this.props.showContinue !== false) {
|
||||
// XXX: button classes
|
||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -389,21 +437,18 @@ export class TermsAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
||||
export class EmailIdentityAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.email.identity";
|
||||
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
authSessionId: PropTypes.string.isRequired,
|
||||
clientSecret: PropTypes.string.isRequired,
|
||||
inputs: PropTypes.object.isRequired,
|
||||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
|
||||
inputs?: {
|
||||
emailAddress?: string;
|
||||
};
|
||||
stageState?: {
|
||||
emailSid: string;
|
||||
};
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
||||
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
|
||||
static LOGIN_TYPE = AuthType.Email;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
|
@ -427,7 +472,7 @@ export class EmailIdentityAuthEntry extends React.Component {
|
|||
return (
|
||||
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
|
||||
{ emailAddress: <b>{ this.props.inputs.emailAddress }</b> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||
|
@ -437,37 +482,44 @@ export class EmailIdentityAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
interface IMsisdnAuthEntryProps extends IAuthEntryProps {
|
||||
inputs: {
|
||||
phoneCountry: string;
|
||||
phoneNumber: string;
|
||||
};
|
||||
clientSecret: string;
|
||||
fail: (error: Error) => void;
|
||||
}
|
||||
|
||||
interface IMsisdnAuthEntryState {
|
||||
token: string;
|
||||
requestingToken: boolean;
|
||||
errorText: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.MsisdnAuthEntry")
|
||||
export class MsisdnAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.msisdn";
|
||||
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Msisdn;
|
||||
|
||||
static propTypes = {
|
||||
inputs: PropTypes.shape({
|
||||
phoneCountry: PropTypes.string,
|
||||
phoneNumber: PropTypes.string,
|
||||
}),
|
||||
fail: PropTypes.func,
|
||||
clientSecret: PropTypes.func,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
};
|
||||
private submitUrl: string;
|
||||
private sid: string;
|
||||
private msisdn: string;
|
||||
|
||||
state = {
|
||||
token: '',
|
||||
requestingToken: false,
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
token: '',
|
||||
requestingToken: false,
|
||||
errorText: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
|
||||
this._submitUrl = null;
|
||||
this._sid = null;
|
||||
this._msisdn = null;
|
||||
this._tokenBox = null;
|
||||
|
||||
this.setState({requestingToken: true});
|
||||
this._requestMsisdnToken().catch((e) => {
|
||||
this.requestMsisdnToken().catch((e) => {
|
||||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
|
@ -477,26 +529,26 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
/*
|
||||
* Requests a verification token by SMS.
|
||||
*/
|
||||
_requestMsisdnToken() {
|
||||
private requestMsisdnToken(): Promise<void> {
|
||||
return this.props.matrixClient.requestRegisterMsisdnToken(
|
||||
this.props.inputs.phoneCountry,
|
||||
this.props.inputs.phoneNumber,
|
||||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
).then((result) => {
|
||||
this._submitUrl = result.submit_url;
|
||||
this._sid = result.sid;
|
||||
this._msisdn = result.msisdn;
|
||||
this.submitUrl = result.submit_url;
|
||||
this.sid = result.sid;
|
||||
this.msisdn = result.msisdn;
|
||||
});
|
||||
}
|
||||
|
||||
_onTokenChange = e => {
|
||||
private onTokenChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
token: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onFormSubmit = async e => {
|
||||
private onFormSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (this.state.token == '') return;
|
||||
|
||||
|
@ -506,20 +558,20 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
|
||||
try {
|
||||
let result;
|
||||
if (this._submitUrl) {
|
||||
if (this.submitUrl) {
|
||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
||||
this.submitUrl, this.sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else {
|
||||
throw new Error("The registration with MSISDN flow is misconfigured");
|
||||
}
|
||||
if (result.success) {
|
||||
const creds = {
|
||||
sid: this._sid,
|
||||
sid: this.sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
};
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
type: AuthType.Msisdn,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
|
@ -543,7 +595,7 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
return <Loader />;
|
||||
} else {
|
||||
const enableSubmit = Boolean(this.state.token);
|
||||
const submitClasses = classnames({
|
||||
const submitClasses = classNames({
|
||||
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||
mx_GeneralButton: true,
|
||||
});
|
||||
|
@ -558,16 +610,16 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||
{ msisdn: <i>{ this._msisdn }</i> },
|
||||
{ msisdn: <i>{ this.msisdn }</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please enter the code it contains:") }</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<input type="text"
|
||||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||
value={this.state.token}
|
||||
onChange={this._onTokenChange}
|
||||
onChange={this.onTokenChange}
|
||||
aria-label={ _t("Code")}
|
||||
/>
|
||||
<br />
|
||||
|
@ -584,40 +636,40 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.SSOAuthEntry")
|
||||
export class SSOAuthEntry extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
authSessionId: PropTypes.string.isRequired,
|
||||
loginType: PropTypes.string.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
continueText: PropTypes.string,
|
||||
continueKind: PropTypes.string,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
interface ISSOAuthEntryProps extends IAuthEntryProps {
|
||||
continueText?: string;
|
||||
continueKind?: string;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
static LOGIN_TYPE = "m.login.sso";
|
||||
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
|
||||
interface ISSOAuthEntryState {
|
||||
phase: number;
|
||||
attemptFailed: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.SSOAuthEntry")
|
||||
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Sso;
|
||||
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
|
||||
|
||||
static PHASE_PREAUTH = 1; // button to start SSO
|
||||
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
||||
|
||||
_ssoUrl: string;
|
||||
private ssoUrl: string;
|
||||
private popupWindow: Window;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// We actually send the user through fallback auth so we don't have to
|
||||
// deal with a redirect back to us, losing application context.
|
||||
this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
||||
this.ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
this.popupWindow = null;
|
||||
window.addEventListener("message", this.onReceiveMessage);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
|
@ -625,44 +677,44 @@ export class SSOAuthEntry extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
window.removeEventListener("message", this.onReceiveMessage);
|
||||
if (this.popupWindow) {
|
||||
this.popupWindow.close();
|
||||
this.popupWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
attemptFailed = () => {
|
||||
public attemptFailed = () => {
|
||||
this.setState({
|
||||
attemptFailed: true,
|
||||
});
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
private onReceiveMessage = (event: MessageEvent) => {
|
||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
if (this.popupWindow) {
|
||||
this.popupWindow.close();
|
||||
this.popupWindow = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onStartAuthClick = () => {
|
||||
private onStartAuthClick = () => {
|
||||
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
||||
// certainly will need to open the thing in a new tab to avoid losing application
|
||||
// context.
|
||||
|
||||
this._popupWindow = window.open(this._ssoUrl, "_blank");
|
||||
this.popupWindow = window.open(this.ssoUrl, "_blank");
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
|
||||
onConfirmClick = () => {
|
||||
private onConfirmClick = () => {
|
||||
this.props.submitAuthDict({});
|
||||
};
|
||||
|
||||
|
@ -716,46 +768,37 @@ export class SSOAuthEntry extends React.Component {
|
|||
}
|
||||
|
||||
@replaceableComponent("views.auth.FallbackAuthEntry")
|
||||
export class FallbackAuthEntry extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
authSessionId: PropTypes.string.isRequired,
|
||||
loginType: PropTypes.string.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
};
|
||||
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||
private popupWindow: Window;
|
||||
private fallbackButton = createRef<HTMLAnchorElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// we have to make the user click a button, as browsers will block
|
||||
// the popup if we open it immediately.
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
|
||||
this._fallbackButton = createRef();
|
||||
this.popupWindow = null;
|
||||
window.addEventListener("message", this.onReceiveMessage);
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
window.removeEventListener("message", this.onReceiveMessage);
|
||||
if (this.popupWindow) {
|
||||
this.popupWindow.close();
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
if (this._fallbackButton.current) {
|
||||
this._fallbackButton.current.focus();
|
||||
public focus = () => {
|
||||
if (this.fallbackButton.current) {
|
||||
this.fallbackButton.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onShowFallbackClick = e => {
|
||||
private onShowFallbackClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -763,10 +806,10 @@ export class FallbackAuthEntry extends React.Component {
|
|||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url, "_blank");
|
||||
this.popupWindow = window.open(url, "_blank");
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
private onReceiveMessage = (event: MessageEvent) => {
|
||||
if (
|
||||
event.data === "authDone" &&
|
||||
event.origin === this.props.matrixClient.getHomeserverUrl()
|
||||
|
@ -786,27 +829,31 @@ export class FallbackAuthEntry extends React.Component {
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
|
||||
_t("Start authentication")
|
||||
}</a>
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const AuthEntryComponents = [
|
||||
PasswordAuthEntry,
|
||||
RecaptchaAuthEntry,
|
||||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
TermsAuthEntry,
|
||||
SSOAuthEntry,
|
||||
];
|
||||
|
||||
export default function getEntryComponentForLoginType(loginType) {
|
||||
for (const c of AuthEntryComponents) {
|
||||
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
|
||||
return c;
|
||||
}
|
||||
export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
|
||||
switch (loginType) {
|
||||
case AuthType.Password:
|
||||
return PasswordAuthEntry;
|
||||
case AuthType.Recaptcha:
|
||||
return RecaptchaAuthEntry;
|
||||
case AuthType.Email:
|
||||
return EmailIdentityAuthEntry;
|
||||
case AuthType.Msisdn:
|
||||
return MsisdnAuthEntry;
|
||||
case AuthType.Terms:
|
||||
return TermsAuthEntry;
|
||||
case AuthType.Sso:
|
||||
case AuthType.SsoUnstable:
|
||||
return SSOAuthEntry;
|
||||
default:
|
||||
return FallbackAuthEntry;
|
||||
}
|
||||
return FallbackAuthEntry;
|
||||
}
|
|
@ -119,7 +119,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
if (this.props.room.roomId !== room.roomId) return;
|
||||
|
||||
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
||||
this.setState({icon: this.calculateIcon()});
|
||||
const newIcon = this.calculateIcon();
|
||||
if (newIcon !== this.state.icon) {
|
||||
this.setState({icon: newIcon});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -212,7 +212,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
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,14 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
||||
import {
|
||||
PHASE_UNSENT,
|
||||
|
@ -30,27 +30,33 @@ import {
|
|||
PHASE_DONE,
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
VerificationRequest,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import {SETTINGS} from "../../../settings/Settings";
|
||||
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
|
||||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { SETTINGS } from "../../../settings/Settings";
|
||||
import SettingsStore, { LEVEL_ORDER } from "../../../settings/SettingsStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { SettingLevel } from '../../../settings/SettingLevel';
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
interface IGenericEditorProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
}
|
||||
interface IGenericEditorState {
|
||||
message?: string;
|
||||
[inputId: string]: boolean | string;
|
||||
}
|
||||
|
||||
onBack() {
|
||||
abstract class GenericEditor<
|
||||
P extends IGenericEditorProps = IGenericEditorProps,
|
||||
S extends IGenericEditorState = IGenericEditorState,
|
||||
> extends React.PureComponent<P, S> {
|
||||
protected onBack = () => {
|
||||
if (this.state.message) {
|
||||
this.setState({ message: null });
|
||||
} else {
|
||||
|
@ -58,47 +64,60 @@ class GenericEditor extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
protected onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
// @ts-ignore: Unsure how to convince TS this is okay when the state
|
||||
// type can be extended.
|
||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||
}
|
||||
|
||||
_buttons() {
|
||||
protected abstract send();
|
||||
|
||||
protected buttons(): React.ReactNode {
|
||||
return <div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||
</div>;
|
||||
}
|
||||
|
||||
textInput(id, label) {
|
||||
protected textInput(id: string, label: string): React.ReactNode {
|
||||
return <Field
|
||||
id={id}
|
||||
label={label}
|
||||
size="42"
|
||||
size={42}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
autoComplete="on"
|
||||
value={this.state[id]}
|
||||
onChange={this._onChange}
|
||||
value={this.state[id] as string}
|
||||
onChange={this.onChange}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export class SendCustomEvent extends GenericEditor {
|
||||
static getLabel() { return _t('Send Custom Event'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
forceStateEvent: PropTypes.bool,
|
||||
forceGeneralEvent: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
interface ISendCustomEventProps extends IGenericEditorProps {
|
||||
room: Room;
|
||||
forceStateEvent?: boolean;
|
||||
forceGeneralEvent?: boolean;
|
||||
inputs?: {
|
||||
eventType?: string;
|
||||
stateKey?: string;
|
||||
evContent?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISendCustomEventState extends IGenericEditorState {
|
||||
isStateEvent: boolean;
|
||||
eventType: string;
|
||||
stateKey: string;
|
||||
evContent: string;
|
||||
}
|
||||
|
||||
export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendCustomEventState> {
|
||||
static getLabel() { return _t('Send Custom Event'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, stateKey, evContent} = Object.assign({
|
||||
eventType: '',
|
||||
|
@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
};
|
||||
}
|
||||
|
||||
send(content) {
|
||||
private doSend(content: object): Promise<void> {
|
||||
const cli = this.context;
|
||||
if (this.state.isStateEvent) {
|
||||
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
||||
|
@ -124,7 +143,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
}
|
||||
}
|
||||
|
||||
async _send() {
|
||||
protected send = async () => {
|
||||
if (this.state.eventType === '') {
|
||||
this.setState({ message: _t('You must specify an event type!') });
|
||||
return;
|
||||
|
@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
let message;
|
||||
try {
|
||||
const content = JSON.parse(this.state.evContent);
|
||||
await this.send(content);
|
||||
await this.doSend(content);
|
||||
message = _t('Event sent!');
|
||||
} catch (e) {
|
||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||
|
@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
<div className="mx_Dialog_content">
|
||||
{ this.state.message }
|
||||
</div>
|
||||
{ this._buttons() }
|
||||
{ this.buttons() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -163,35 +182,51 @@ export class SendCustomEvent extends GenericEditor {
|
|||
<br />
|
||||
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||
{ showTglFlip && <div style={{float: "right"}}>
|
||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
|
||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||
type="checkbox"
|
||||
checked={this.state.isStateEvent}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label className="mx_DevTools_tgl-btn"
|
||||
data-tg-off="Event"
|
||||
data-tg-on="State Event"
|
||||
htmlFor="isStateEvent"
|
||||
/>
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class SendAccountData extends GenericEditor {
|
||||
static getLabel() { return _t('Send Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
isRoomAccountData: PropTypes.bool,
|
||||
forceMode: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
interface ISendAccountDataProps extends IGenericEditorProps {
|
||||
room: Room;
|
||||
isRoomAccountData: boolean;
|
||||
forceMode: boolean;
|
||||
inputs?: {
|
||||
eventType?: string;
|
||||
evContent?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISendAccountDataState extends IGenericEditorState {
|
||||
isRoomAccountData: boolean;
|
||||
eventType: string;
|
||||
evContent: string;
|
||||
}
|
||||
|
||||
class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountDataState> {
|
||||
static getLabel() { return _t('Send Account Data'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, evContent} = Object.assign({
|
||||
eventType: '',
|
||||
|
@ -206,7 +241,7 @@ class SendAccountData extends GenericEditor {
|
|||
};
|
||||
}
|
||||
|
||||
send(content) {
|
||||
private doSend(content: object): Promise<void> {
|
||||
const cli = this.context;
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
||||
|
@ -214,7 +249,7 @@ class SendAccountData extends GenericEditor {
|
|||
return cli.setAccountData(this.state.eventType, content);
|
||||
}
|
||||
|
||||
async _send() {
|
||||
protected send = async () => {
|
||||
if (this.state.eventType === '') {
|
||||
this.setState({ message: _t('You must specify an event type!') });
|
||||
return;
|
||||
|
@ -223,7 +258,7 @@ class SendAccountData extends GenericEditor {
|
|||
let message;
|
||||
try {
|
||||
const content = JSON.parse(this.state.evContent);
|
||||
await this.send(content);
|
||||
await this.doSend(content);
|
||||
message = _t('Event sent!');
|
||||
} catch (e) {
|
||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||
|
@ -237,7 +272,7 @@ class SendAccountData extends GenericEditor {
|
|||
<div className="mx_Dialog_content">
|
||||
{ this.state.message }
|
||||
</div>
|
||||
{ this._buttons() }
|
||||
{ this.buttons() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -247,14 +282,23 @@ class SendAccountData extends GenericEditor {
|
|||
<br />
|
||||
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||
type="checkbox"
|
||||
checked={this.state.isRoomAccountData}
|
||||
disabled={this.props.forceMode}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label className="mx_DevTools_tgl-btn"
|
||||
data-tg-off="Account Data"
|
||||
data-tg-on="Room Data"
|
||||
htmlFor="isRoomAccountData"
|
||||
/>
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -264,17 +308,22 @@ class SendAccountData extends GenericEditor {
|
|||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
query: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
interface IFilteredListProps {
|
||||
children: React.ReactElement[];
|
||||
query: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
static filterChildren(children, query) {
|
||||
interface IFilteredListState {
|
||||
filteredChildren: React.ReactElement[];
|
||||
truncateAt: number;
|
||||
}
|
||||
|
||||
class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredListState> {
|
||||
static filterChildren(children: React.ReactElement[], query: string): React.ReactElement[] {
|
||||
if (!query) return children;
|
||||
const lcQuery = query.toLowerCase();
|
||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
return children.filter((child) => child.key.toString().toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -295,27 +344,27 @@ class FilteredList extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
private showAll = () => {
|
||||
this.setState({
|
||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||
});
|
||||
};
|
||||
|
||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
private createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||
</button>;
|
||||
};
|
||||
|
||||
onQuery = (ev) => {
|
||||
private onQuery = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||
};
|
||||
|
||||
getChildren = (start: number, end: number) => {
|
||||
private getChildren = (start: number, end: number): React.ReactElement[] => {
|
||||
return this.state.filteredChildren.slice(start, end);
|
||||
};
|
||||
|
||||
getChildCount = (): number => {
|
||||
private getChildCount = (): number => {
|
||||
return this.state.filteredChildren.length;
|
||||
};
|
||||
|
||||
|
@ -336,28 +385,31 @@ class FilteredList extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class RoomStateExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Room State'); }
|
||||
interface IExplorerProps {
|
||||
room: Room;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
interface IRoomStateExplorerState {
|
||||
eventType?: string;
|
||||
event?: MatrixEvent;
|
||||
editing: boolean;
|
||||
queryEventType: string;
|
||||
queryStateKey: string;
|
||||
}
|
||||
|
||||
class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateExplorerState> {
|
||||
static getLabel() { return _t('Explore Room State'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
||||
private roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.roomStateEvents = this.props.room.currentState.events;
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
||||
this.onQueryStateKey = this.onQueryStateKey.bind(this);
|
||||
|
||||
this.state = {
|
||||
eventType: null,
|
||||
event: null,
|
||||
|
@ -368,19 +420,19 @@ class RoomStateExplorer extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
browseEventType(eventType) {
|
||||
private browseEventType(eventType: string) {
|
||||
return () => {
|
||||
this.setState({ eventType });
|
||||
};
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
private onViewSourceClick(event: MatrixEvent) {
|
||||
return () => {
|
||||
this.setState({ event });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
private onBack = () => {
|
||||
if (this.state.editing) {
|
||||
this.setState({ editing: false });
|
||||
} else if (this.state.event) {
|
||||
|
@ -392,15 +444,15 @@ class RoomStateExplorer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
editEv() {
|
||||
private editEv = () => {
|
||||
this.setState({ editing: true });
|
||||
}
|
||||
|
||||
onQueryEventType(filterEventType) {
|
||||
private onQueryEventType = (filterEventType: string) => {
|
||||
this.setState({ queryEventType: filterEventType });
|
||||
}
|
||||
|
||||
onQueryStateKey(filterStateKey) {
|
||||
private onQueryStateKey = (filterStateKey: string) => {
|
||||
this.setState({ queryStateKey: filterStateKey });
|
||||
}
|
||||
|
||||
|
@ -472,24 +524,22 @@ class RoomStateExplorer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class AccountDataExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Account Data'); }
|
||||
interface IAccountDataExplorerState {
|
||||
isRoomAccountData: boolean;
|
||||
event?: MatrixEvent;
|
||||
editing: boolean;
|
||||
queryEventType: string;
|
||||
[inputId: string]: boolean | string;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
|
||||
static getLabel() { return _t('Explore Account Data'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
||||
|
||||
this.state = {
|
||||
isRoomAccountData: false,
|
||||
event: null,
|
||||
|
@ -499,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
getData() {
|
||||
private getData(): Record<string, MatrixEvent> {
|
||||
if (this.state.isRoomAccountData) {
|
||||
return this.props.room.accountData;
|
||||
}
|
||||
return this.context.store.accountData;
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
private onViewSourceClick(event: MatrixEvent) {
|
||||
return () => {
|
||||
this.setState({ event });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
private onBack = () => {
|
||||
if (this.state.editing) {
|
||||
this.setState({ editing: false });
|
||||
} else if (this.state.event) {
|
||||
|
@ -522,15 +572,15 @@ class AccountDataExplorer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
private onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||
}
|
||||
|
||||
editEv() {
|
||||
private editEv = () => {
|
||||
this.setState({ editing: true });
|
||||
}
|
||||
|
||||
onQueryEventType(queryEventType) {
|
||||
private onQueryEventType = (queryEventType: string) => {
|
||||
this.setState({ queryEventType });
|
||||
}
|
||||
|
||||
|
@ -580,30 +630,39 @@ class AccountDataExplorer extends React.PureComponent {
|
|||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
||||
</div> }
|
||||
<div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||
type="checkbox"
|
||||
checked={this.state.isRoomAccountData}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label className="mx_DevTools_tgl-btn"
|
||||
data-tg-off="Account Data"
|
||||
data-tg-on="Room Data"
|
||||
htmlFor="isRoomAccountData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class ServersInRoomList extends React.PureComponent {
|
||||
interface IServersInRoomListState {
|
||||
query: string;
|
||||
}
|
||||
|
||||
class ServersInRoomList extends React.PureComponent<IExplorerProps, IServersInRoomListState> {
|
||||
static getLabel() { return _t('View Servers in Room'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private servers: React.ReactElement[];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = this.props.room;
|
||||
const servers = new Set();
|
||||
const servers = new Set<string>();
|
||||
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
||||
this.servers = Array.from(servers).map(s =>
|
||||
<button key={s} className="mx_DevTools_ServersInRoomList_button">
|
||||
|
@ -615,7 +674,7 @@ class ServersInRoomList extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
onQuery = (query) => {
|
||||
private onQuery = (query: string) => {
|
||||
this.setState({ query });
|
||||
}
|
||||
|
||||
|
@ -642,7 +701,10 @@ const PHASE_MAP = {
|
|||
[PHASE_CANCELLED]: "cancelled",
|
||||
};
|
||||
|
||||
function VerificationRequest({txnId, request}) {
|
||||
const VerificationRequestExplorer: React.FC<{
|
||||
txnId: string;
|
||||
request: VerificationRequest;
|
||||
}> = ({txnId, request}) => {
|
||||
const [, updateState] = useState();
|
||||
const [timeout, setRequestTimeout] = useState(request.timeout);
|
||||
|
||||
|
@ -679,7 +741,7 @@ function VerificationRequest({txnId, request}) {
|
|||
</div>);
|
||||
}
|
||||
|
||||
class VerificationExplorer extends React.Component {
|
||||
class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||
static getLabel() {
|
||||
return _t("Verification Requests");
|
||||
}
|
||||
|
@ -687,7 +749,7 @@ class VerificationExplorer extends React.Component {
|
|||
/* Ensure this.context is the cli */
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
onNewRequest = () => {
|
||||
private onNewRequest = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
|
@ -710,7 +772,7 @@ class VerificationExplorer extends React.Component {
|
|||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
||||
<VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -720,7 +782,12 @@ class VerificationExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class WidgetExplorer extends React.Component {
|
||||
interface IWidgetExplorerState {
|
||||
query: string;
|
||||
editWidget?: IApp;
|
||||
}
|
||||
|
||||
class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerState> {
|
||||
static getLabel() {
|
||||
return _t("Active Widgets");
|
||||
}
|
||||
|
@ -734,19 +801,19 @@ class WidgetExplorer extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onWidgetStoreUpdate = () => {
|
||||
private onWidgetStoreUpdate = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onQueryChange = (query) => {
|
||||
private onQueryChange = (query: string) => {
|
||||
this.setState({query});
|
||||
};
|
||||
|
||||
onEditWidget = (widget) => {
|
||||
private onEditWidget = (widget: IApp) => {
|
||||
this.setState({editWidget: widget});
|
||||
};
|
||||
|
||||
onBack = () => {
|
||||
private onBack = () => {
|
||||
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
||||
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
||||
this.setState({editWidget: null});
|
||||
|
@ -769,8 +836,11 @@ class WidgetExplorer extends React.Component {
|
|||
const editWidget = this.state.editWidget;
|
||||
const widgets = WidgetStore.instance.getApps(room.roomId);
|
||||
if (editWidget && widgets.includes(editWidget)) {
|
||||
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values()))
|
||||
.reduce((p, c) => {p.push(...c); return p;}, []);
|
||||
const allState = Array.from(
|
||||
Array.from(room.currentState.events.values()).map((e: Map<string, MatrixEvent>) => {
|
||||
return e.values();
|
||||
}),
|
||||
).reduce((p, c) => { p.push(...c); return p; }, []);
|
||||
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
||||
if (!stateEv) { // "should never happen"
|
||||
return <div>
|
||||
|
@ -811,7 +881,15 @@ class WidgetExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class SettingsExplorer extends React.Component {
|
||||
interface ISettingsExplorerState {
|
||||
query: string;
|
||||
editSetting?: string;
|
||||
viewSetting?: string;
|
||||
explicitValues?: string;
|
||||
explicitRoomValues?: string;
|
||||
}
|
||||
|
||||
class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExplorerState> {
|
||||
static getLabel() {
|
||||
return _t("Settings Explorer");
|
||||
}
|
||||
|
@ -829,19 +907,19 @@ class SettingsExplorer extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onQueryChange = (ev) => {
|
||||
private onQueryChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({query: ev.target.value});
|
||||
};
|
||||
|
||||
onExplValuesEdit = (ev) => {
|
||||
private onExplValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({explicitValues: ev.target.value});
|
||||
};
|
||||
|
||||
onExplRoomValuesEdit = (ev) => {
|
||||
private onExplRoomValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({explicitRoomValues: ev.target.value});
|
||||
};
|
||||
|
||||
onBack = () => {
|
||||
private onBack = () => {
|
||||
if (this.state.editSetting) {
|
||||
this.setState({editSetting: null});
|
||||
} else if (this.state.viewSetting) {
|
||||
|
@ -851,12 +929,12 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onViewClick = (ev, settingId) => {
|
||||
private onViewClick = (ev: MouseEvent, settingId: string) => {
|
||||
ev.preventDefault();
|
||||
this.setState({viewSetting: settingId});
|
||||
};
|
||||
|
||||
onEditClick = (ev, settingId) => {
|
||||
private onEditClick = (ev: MouseEvent, settingId: string) => {
|
||||
ev.preventDefault();
|
||||
this.setState({
|
||||
editSetting: settingId,
|
||||
|
@ -865,7 +943,7 @@ class SettingsExplorer extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onSaveClick = async () => {
|
||||
private onSaveClick = async () => {
|
||||
try {
|
||||
const settingId = this.state.editSetting;
|
||||
const parsedExplicit = JSON.parse(this.state.explicitValues);
|
||||
|
@ -874,7 +952,7 @@ class SettingsExplorer extends React.Component {
|
|||
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
||||
try {
|
||||
const val = parsedExplicit[level];
|
||||
await SettingsStore.setValue(settingId, null, level, val);
|
||||
await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
@ -884,7 +962,7 @@ class SettingsExplorer extends React.Component {
|
|||
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
||||
try {
|
||||
const val = parsedExplicitRoom[level];
|
||||
await SettingsStore.setValue(settingId, roomId, level, val);
|
||||
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
@ -901,7 +979,7 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
renderSettingValue(val) {
|
||||
private renderSettingValue(val: any): string {
|
||||
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
|
||||
const toStringTypes = ['boolean', 'number'];
|
||||
if (toStringTypes.includes(typeof(val))) {
|
||||
|
@ -911,7 +989,7 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
renderExplicitSettingValues(setting, roomId) {
|
||||
private renderExplicitSettingValues(setting: string, roomId: string): string {
|
||||
const vals = {};
|
||||
for (const level of LEVEL_ORDER) {
|
||||
try {
|
||||
|
@ -926,7 +1004,7 @@ class SettingsExplorer extends React.Component {
|
|||
return JSON.stringify(vals, null, 4);
|
||||
}
|
||||
|
||||
renderCanEditLevel(roomId, level) {
|
||||
private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode {
|
||||
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
|
||||
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
|
||||
return <td className={className}><code>{canEdit.toString()}</code></td>;
|
||||
|
@ -1062,27 +1140,37 @@ class SettingsExplorer extends React.Component {
|
|||
|
||||
<div>
|
||||
{_t("Value:")}
|
||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
|
||||
<code>{this.renderSettingValue(
|
||||
SettingsStore.getValue(this.state.viewSetting),
|
||||
)}</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Value in this room:")}
|
||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code>
|
||||
<code>{this.renderSettingValue(
|
||||
SettingsStore.getValue(this.state.viewSetting, room.roomId),
|
||||
)}</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Values at explicit levels:")}
|
||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre>
|
||||
<pre><code>{this.renderExplicitSettingValues(
|
||||
this.state.viewSetting, null,
|
||||
)}</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Values at explicit levels in this room:")}
|
||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre>
|
||||
<pre><code>{this.renderExplicitSettingValues(
|
||||
this.state.viewSetting, room.roomId,
|
||||
)}</code></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button>
|
||||
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{
|
||||
_t("Edit Values")
|
||||
}</button>
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1091,7 +1179,11 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
type DevtoolsDialogEntry = React.JSXElementConstructor<any> & {
|
||||
getLabel: () => string;
|
||||
};
|
||||
|
||||
const Entries: DevtoolsDialogEntry[] = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
SendAccountData,
|
||||
|
@ -1102,43 +1194,36 @@ const Entries = [
|
|||
SettingsExplorer,
|
||||
];
|
||||
|
||||
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onFinished: (finished: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mode?: DevtoolsDialogEntry;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
||||
export default class DevtoolsDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
|
||||
this.state = {
|
||||
mode: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_setMode(mode) {
|
||||
private setMode(mode: DevtoolsDialogEntry) {
|
||||
return () => {
|
||||
this.setState({ mode });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
if (this.prevMode) {
|
||||
this.setState({ mode: this.prevMode });
|
||||
this.prevMode = null;
|
||||
} else {
|
||||
this.setState({ mode: null });
|
||||
}
|
||||
private onBack = () => {
|
||||
this.setState({ mode: null });
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
|
@ -1165,7 +1250,7 @@ export default class DevtoolsDialog extends React.PureComponent {
|
|||
<div className="mx_Dialog_content">
|
||||
{ Entries.map((Entry) => {
|
||||
const label = Entry.getLabel();
|
||||
const onClick = this._setMode(Entry);
|
||||
const onClick = this.setMode(Entry);
|
||||
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
|
||||
}) }
|
||||
</div>
|
|
@ -47,10 +47,19 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {getAddressType} from "../../../UserAddress";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
interface IRecentUser {
|
||||
userId: string,
|
||||
user: RoomMember,
|
||||
lastActive: number,
|
||||
}
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
@ -61,43 +70,41 @@ const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is c
|
|||
// This is the interface that is expected by various components in this file. It is a bit
|
||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// for 3PIDs/email addresses.
|
||||
//
|
||||
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
||||
class Member {
|
||||
abstract class Member {
|
||||
/**
|
||||
* The display name of this Member. For users this should be their profile's display
|
||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||
*/
|
||||
get name(): string { throw new Error("Member class not implemented"); }
|
||||
public abstract get name(): string;
|
||||
|
||||
/**
|
||||
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||
* be the 3PID address (email).
|
||||
*/
|
||||
get userId(): string { throw new Error("Member class not implemented"); }
|
||||
public abstract get userId(): string;
|
||||
|
||||
/**
|
||||
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||
*/
|
||||
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
||||
public abstract getMxcAvatarUrl(): string;
|
||||
}
|
||||
|
||||
class DirectoryMember extends Member {
|
||||
_userId: string;
|
||||
_displayName: string;
|
||||
_avatarUrl: string;
|
||||
private readonly _userId: string;
|
||||
private readonly displayName: string;
|
||||
private readonly avatarUrl: string;
|
||||
|
||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this._displayName = userDirResult.display_name;
|
||||
this._avatarUrl = userDirResult.avatar_url;
|
||||
this.displayName = userDirResult.display_name;
|
||||
this.avatarUrl = userDirResult.avatar_url;
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._displayName || this._userId;
|
||||
return this.displayName || this._userId;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
|
@ -105,32 +112,32 @@ class DirectoryMember extends Member {
|
|||
}
|
||||
|
||||
getMxcAvatarUrl(): string {
|
||||
return this._avatarUrl;
|
||||
return this.avatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
class ThreepidMember extends Member {
|
||||
_id: string;
|
||||
private readonly id: string;
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this._id = id;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
// This is a getter that would be falsey on all other implementations. Until we have
|
||||
// better type support in the react-sdk we can use this trick to determine the kind
|
||||
// of 3PID we're dealing with, if any.
|
||||
get isEmail(): boolean {
|
||||
return this._id.includes('@');
|
||||
return this.id.includes('@');
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._id;
|
||||
return this.id;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
return this._id;
|
||||
return this.id;
|
||||
}
|
||||
|
||||
getMxcAvatarUrl(): string {
|
||||
|
@ -140,11 +147,11 @@ class ThreepidMember extends Member {
|
|||
|
||||
interface IDMUserTileProps {
|
||||
member: RoomMember;
|
||||
onRemove: (RoomMember) => any;
|
||||
onRemove(member: RoomMember): void;
|
||||
}
|
||||
|
||||
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -153,9 +160,6 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const avatarSize = 20;
|
||||
const avatar = this.props.member.isEmail
|
||||
? <img
|
||||
|
@ -177,7 +181,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
closeButton = (
|
||||
<AccessibleButton
|
||||
className='mx_InviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
onClick={this.onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
||||
alt={_t('Remove')} width={8} height={8}
|
||||
|
@ -201,13 +205,13 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
interface IDMRoomTileProps {
|
||||
member: RoomMember;
|
||||
lastActiveTs: number;
|
||||
onToggle: (RoomMember) => any;
|
||||
onToggle(member: RoomMember): void;
|
||||
highlightWord: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||
_onClick = (e) => {
|
||||
private onClick = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -215,7 +219,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
this.props.onToggle(this.props.member);
|
||||
};
|
||||
|
||||
_highlightName(str: string) {
|
||||
private highlightName(str: string) {
|
||||
if (!this.props.highlightWord) return str;
|
||||
|
||||
// We convert things to lowercase for index searching, but pull substrings from
|
||||
|
@ -252,8 +256,6 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
let timestamp = null;
|
||||
if (this.props.lastActiveTs) {
|
||||
const humanTs = humanizeTime(this.props.lastActiveTs);
|
||||
|
@ -291,13 +293,13 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
|
||||
const caption = this.props.member.isEmail
|
||||
? _t("Invite by email")
|
||||
: this._highlightName(this.props.member.userId);
|
||||
: this.highlightName(this.props.member.userId);
|
||||
|
||||
return (
|
||||
<div className='mx_InviteDialog_roomTile' onClick={this._onClick}>
|
||||
<div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
|
||||
{stackedAvatar}
|
||||
<span className="mx_InviteDialog_roomTile_nameStack">
|
||||
<div className='mx_InviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</div>
|
||||
<div className='mx_InviteDialog_roomTile_name'>{this.highlightName(this.props.member.name)}</div>
|
||||
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
||||
</span>
|
||||
{timestamp}
|
||||
|
@ -308,7 +310,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
|
||||
interface IInviteDialogProps {
|
||||
// Takes an array of user IDs/emails to invite.
|
||||
onFinished: (toInvite?: string[]) => any;
|
||||
onFinished: (toInvite?: string[]) => void;
|
||||
|
||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||
// not provided.
|
||||
|
@ -349,8 +351,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
initialText: "",
|
||||
};
|
||||
|
||||
_debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||
_editorRef: any = null;
|
||||
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||
private editorRef = createRef<HTMLInputElement>();
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -378,7 +381,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
filterText: this.props.initialText,
|
||||
recents: InviteDialog.buildRecents(alreadyInvited),
|
||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
suggestions: this._buildSuggestions(alreadyInvited),
|
||||
suggestions: this.buildSuggestions(alreadyInvited),
|
||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||
serverResultsMixin: [],
|
||||
threepidResultsMixin: [],
|
||||
|
@ -390,21 +393,23 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
busy: false,
|
||||
errorText: null,
|
||||
};
|
||||
|
||||
this._editorRef = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.initialText) {
|
||||
this._updateSuggestions(this.props.initialText);
|
||||
this.updateSuggestions(this.props.initialText);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private onConsultFirstChange = (ev) => {
|
||||
this.setState({consultFirst: ev.target.checked});
|
||||
}
|
||||
|
||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
||||
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
||||
|
@ -467,7 +472,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return recents;
|
||||
}
|
||||
|
||||
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
||||
private buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
||||
const maxConsideredMembers = 200;
|
||||
const joinedRooms = MatrixClientPeg.get().getRooms()
|
||||
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
||||
|
@ -574,7 +579,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
members.sort((a, b) => {
|
||||
if (a.score === b.score) {
|
||||
if (a.numRooms === b.numRooms) {
|
||||
return a.member.userId.localeCompare(b.member.userId);
|
||||
return compare(a.member.userId, b.member.userId);
|
||||
}
|
||||
|
||||
return b.numRooms - a.numRooms;
|
||||
|
@ -585,7 +590,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||
}
|
||||
|
||||
_shouldAbortAfterInviteError(result): boolean {
|
||||
private shouldAbortAfterInviteError(result): boolean {
|
||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
||||
if (failedUsers.length > 0) {
|
||||
console.log("Failed to invite users: ", result);
|
||||
|
@ -600,7 +605,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return false;
|
||||
}
|
||||
|
||||
_convertFilter(): Member[] {
|
||||
private convertFilter(): Member[] {
|
||||
// Check to see if there's anything to convert first
|
||||
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
||||
|
||||
|
@ -617,10 +622,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return newTargets;
|
||||
}
|
||||
|
||||
_startDm = async () => {
|
||||
private startDm = async () => {
|
||||
this.setState({busy: true});
|
||||
const client = MatrixClientPeg.get();
|
||||
const targets = this._convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
|
@ -694,11 +699,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_inviteUsers = async () => {
|
||||
private inviteUsers = async () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
this.setState({busy: true});
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
this.convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -715,7 +720,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
try {
|
||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
|
@ -749,9 +754,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_transferCall = async () => {
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
private transferCall = async () => {
|
||||
this.convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
if (targetIds.length > 1) {
|
||||
this.setState({
|
||||
|
@ -790,26 +795,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_onKeyDown = (e) => {
|
||||
private onKeyDown = (e) => {
|
||||
if (this.state.busy) return;
|
||||
const value = e.target.value.trim();
|
||||
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
|
||||
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
|
||||
// when the field is empty and the user hits backspace remove the right-most target
|
||||
e.preventDefault();
|
||||
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||
this.removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
||||
// when the user hits enter with something in their field try to convert it
|
||||
e.preventDefault();
|
||||
this._convertFilter();
|
||||
this.convertFilter();
|
||||
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
|
||||
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
|
||||
e.preventDefault();
|
||||
this._convertFilter();
|
||||
this.convertFilter();
|
||||
}
|
||||
};
|
||||
|
||||
_updateSuggestions = async (term) => {
|
||||
private updateSuggestions = async (term) => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
|
@ -918,30 +923,30 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
private updateFilter = (e) => {
|
||||
const term = e.target.value;
|
||||
this.setState({filterText: term});
|
||||
|
||||
// Debounce server lookups to reduce spam. We don't clear the existing server
|
||||
// results because they might still be vaguely accurate, likewise for races which
|
||||
// could happen here.
|
||||
if (this._debounceTimer) {
|
||||
clearTimeout(this._debounceTimer);
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
this._debounceTimer = setTimeout(() => {
|
||||
this._updateSuggestions(term);
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this.updateSuggestions(term);
|
||||
}, 150); // 150ms debounce (human reaction time + some)
|
||||
};
|
||||
|
||||
_showMoreRecents = () => {
|
||||
private showMoreRecents = () => {
|
||||
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_showMoreSuggestions = () => {
|
||||
private showMoreSuggestions = () => {
|
||||
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_toggleMember = (member: Member) => {
|
||||
private toggleMember = (member: Member) => {
|
||||
if (!this.state.busy) {
|
||||
let filterText = this.state.filterText;
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
|
@ -954,13 +959,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
this.setState({targets, filterText});
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
if (this.editorRef && this.editorRef.current) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_removeMember = (member: Member) => {
|
||||
private removeMember = (member: Member) => {
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) {
|
||||
|
@ -968,12 +973,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.setState({targets});
|
||||
}
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
if (this.editorRef && this.editorRef.current) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onPaste = async (e) => {
|
||||
private onPaste = async (e) => {
|
||||
if (this.state.filterText) {
|
||||
// if the user has already typed something, just let them
|
||||
// paste normally.
|
||||
|
@ -1027,6 +1032,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
failed.push(address);
|
||||
}
|
||||
}
|
||||
if (this.unmounted) return;
|
||||
|
||||
if (failed.length > 0) {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
|
@ -1043,17 +1049,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.setState({targets: [...this.state.targets, ...toAdd]});
|
||||
};
|
||||
|
||||
_onClickInputArea = (e) => {
|
||||
private onClickInputArea = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
if (this.editorRef && this.editorRef.current) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onUseDefaultIdentityServerClick = (e) => {
|
||||
private onUseDefaultIdentityServerClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Update the IS in account data. Actually using it may trigger terms.
|
||||
|
@ -1062,21 +1068,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
||||
};
|
||||
|
||||
_onManageSettingsClick = (e) => {
|
||||
private onManageSettingsClick = (e) => {
|
||||
e.preventDefault();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onCommunityInviteClick = (e) => {
|
||||
private onCommunityInviteClick = (e) => {
|
||||
this.props.onFinished();
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
};
|
||||
|
||||
_renderSection(kind: "recents"|"suggestions") {
|
||||
private renderSection(kind: "recents"|"suggestions") {
|
||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
||||
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
|
||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||
let sectionSubname = null;
|
||||
|
@ -1156,7 +1162,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
member={r.user}
|
||||
lastActiveTs={lastActive(r)}
|
||||
key={r.userId}
|
||||
onToggle={this._toggleMember}
|
||||
onToggle={this.toggleMember}
|
||||
highlightWord={this.state.filterText}
|
||||
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||
/>
|
||||
|
@ -1171,32 +1177,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
);
|
||||
}
|
||||
|
||||
_renderEditor() {
|
||||
private renderEditor() {
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
||||
));
|
||||
const input = (
|
||||
<input
|
||||
type="text"
|
||||
onKeyDown={this._onKeyDown}
|
||||
onChange={this._updateFilter}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.updateFilter}
|
||||
value={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
ref={this.editorRef}
|
||||
onPaste={this.onPaste}
|
||||
autoFocus={true}
|
||||
disabled={this.state.busy}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className='mx_InviteDialog_editor' onClick={this._onClickInputArea}>
|
||||
<div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
|
||||
{targets}
|
||||
{input}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderIdentityServerWarning() {
|
||||
private renderIdentityServerWarning() {
|
||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
||||
!SettingsStore.getValue(UIFeature.IdentityServer)
|
||||
) {
|
||||
|
@ -1214,8 +1220,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
|
@ -1225,7 +1231,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
|
@ -1298,7 +1304,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={this._onCommunityInviteClick}
|
||||
onClick={this.onCommunityInviteClick}
|
||||
>{sub}</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
@ -1309,7 +1315,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
</React.Fragment>;
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
goButtonFn = this.startDm;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||
|
@ -1348,7 +1354,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
});
|
||||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
goButtonFn = this.inviteUsers;
|
||||
|
||||
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
|
@ -1370,7 +1376,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this._transferCall;
|
||||
goButtonFn = this.transferCall;
|
||||
consultSection = <div>
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||
|
@ -1393,7 +1399,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
<div className='mx_InviteDialog_content'>
|
||||
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||
<div className='mx_InviteDialog_addressBar'>
|
||||
{this._renderEditor()}
|
||||
{this.renderEditor()}
|
||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
|
@ -1407,11 +1413,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
</div>
|
||||
</div>
|
||||
{keySharingWarning}
|
||||
{this._renderIdentityServerWarning()}
|
||||
{this.renderIdentityServerWarning()}
|
||||
<div className='error'>{this.state.errorText}</div>
|
||||
<div className='mx_InviteDialog_userSections'>
|
||||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
{this.renderSection('recents')}
|
||||
{this.renderSection('suggestions')}
|
||||
</div>
|
||||
{consultSection}
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,6 @@ import ToggleSwitch from "../elements/ToggleSwitch";
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
|
||||
|
@ -74,9 +73,13 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
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) {
|
||||
|
@ -91,7 +94,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||
}
|
||||
|
||||
const results = await allSettled(promises);
|
||||
const results = await Promise.allSettled(promises);
|
||||
setBusy(false);
|
||||
const failures = results.filter(r => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
|
|
|
@ -38,13 +38,15 @@ import withValidation from "../elements/Validation";
|
|||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import TextInputDialog from "../dialogs/TextInputDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { compare } from "../../../utils/strings";
|
||||
|
||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||
|
||||
const SETTING_NAME = "room_directory_servers";
|
||||
|
||||
const inPlaceOf = (elementRect: Pick<DOMRect, "right" | "top">) => ({
|
||||
right: window.innerWidth - elementRect.right,
|
||||
right: UIStore.instance.windowWidth - elementRect.right,
|
||||
top: elementRect.top,
|
||||
chevronOffset: 0,
|
||||
chevronFace: ChevronFace.None,
|
||||
|
@ -186,7 +188,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
|||
|
||||
protocolsList.forEach(({instances=[]}) => {
|
||||
[...instances].sort((b, a) => {
|
||||
return a.desc.localeCompare(b.desc);
|
||||
return compare(a.desc, b.desc);
|
||||
}).forEach(({desc, instance_id: instanceId}) => {
|
||||
entries.push(
|
||||
<MenuItemRadio
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
||||
import {CHAT_EFFECTS} from '../../../effects'
|
||||
import { CHAT_EFFECTS } from '../../../effects'
|
||||
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||
|
||||
interface IProps {
|
||||
roomWidth: number;
|
||||
|
@ -45,8 +46,8 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
|||
|
||||
useEffect(() => {
|
||||
const resize = () => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.height = window.innerHeight;
|
||||
if (canvasRef.current && canvasRef.current?.height !== UIStore.instance.windowHeight) {
|
||||
canvasRef.current.height = UIStore.instance.windowHeight;
|
||||
}
|
||||
};
|
||||
const onAction = (payload: { action: string }) => {
|
||||
|
@ -58,12 +59,12 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
|||
}
|
||||
const dispatcherRef = dis.register(onAction);
|
||||
const canvas = canvasRef.current;
|
||||
canvas.height = window.innerHeight;
|
||||
window.addEventListener('resize', resize, true);
|
||||
canvas.height = UIStore.instance.windowHeight;
|
||||
UIStore.instance.on(UI_EVENTS.Resize, resize);
|
||||
|
||||
return () => {
|
||||
dis.unregister(dispatcherRef);
|
||||
window.removeEventListener('resize', resize);
|
||||
UIStore.instance.off(UI_EVENTS.Resize, resize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
||||
for (const effect in currentEffects) {
|
||||
|
|
|
@ -18,19 +18,29 @@ import React from "react";
|
|||
import {_t} from "../../../languageHandler";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.elements.InlineSpinner")
|
||||
export default class InlineSpinner extends React.Component {
|
||||
render() {
|
||||
const w = this.props.w || 16;
|
||||
const h = this.props.h || 16;
|
||||
interface IProps {
|
||||
w?: number;
|
||||
h?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.InlineSpinner")
|
||||
export default class InlineSpinner extends React.PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
w: 16,
|
||||
h: 16,
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_InlineSpinner">
|
||||
<div
|
||||
className="mx_InlineSpinner_icon mx_Spinner_icon"
|
||||
style={{width: w, height: h}}
|
||||
style={{width: this.props.w, height: this.props.h}}
|
||||
aria-label={_t("Loading...")}
|
||||
></div>
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -112,7 +112,7 @@ interface IProps {
|
|||
const MAX_PER_ROW = 6;
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
|
||||
const providers = flow.identity_providers || [];
|
||||
if (providers.length < 2) {
|
||||
return <div className="mx_SSOButtons">
|
||||
<SSOButton
|
||||
|
|
|
@ -22,6 +22,7 @@ import React, {Component, CSSProperties} from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
||||
|
@ -69,7 +70,10 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this.renderTooltip, true);
|
||||
window.addEventListener('scroll', this.renderTooltip, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
|
||||
|
@ -84,7 +88,9 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
public componentWillUnmount() {
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this.renderTooltip, true);
|
||||
window.removeEventListener('scroll', this.renderTooltip, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
private updatePosition(style: CSSProperties) {
|
||||
|
@ -97,15 +103,15 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
// we need so that we're still centered.
|
||||
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
||||
}
|
||||
|
||||
const width = UIStore.instance.windowWidth;
|
||||
const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset;
|
||||
const top = baseTop + offset;
|
||||
const right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
|
||||
const right = width - parentBox.right - window.pageXOffset - 16;
|
||||
const left = parentBox.right + window.pageXOffset + 6;
|
||||
const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2);
|
||||
switch (this.props.alignment) {
|
||||
case Alignment.Natural:
|
||||
if (parentBox.right > window.innerWidth / 2) {
|
||||
if (parentBox.right > width / 2) {
|
||||
style.right = right;
|
||||
style.top = top;
|
||||
break;
|
||||
|
|
|
@ -19,19 +19,30 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.elements.TooltipButton")
|
||||
export default class TooltipButton extends React.Component {
|
||||
state = {
|
||||
hover: false,
|
||||
};
|
||||
interface IProps {
|
||||
helpText: string;
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.TooltipButton")
|
||||
export default class TooltipButton extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onMouseOver = () => {
|
||||
this.setState({
|
||||
hover: true,
|
||||
});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
private onMouseLeave = () => {
|
||||
this.setState({
|
||||
hover: false,
|
||||
});
|
|
@ -71,10 +71,14 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
|
|||
|
||||
// We should have a buffer to work with now: let's set it up
|
||||
const playback = new Playback(buffer, waveform);
|
||||
this.setState({playback});
|
||||
this.setState({ playback });
|
||||
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.state.playback?.destroy();
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.error) {
|
||||
// TODO: @@TR: Verify error state
|
||||
|
|
|
@ -81,19 +81,39 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
|||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
myReactions: this.getMyReactions(),
|
||||
showAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidMount() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
if (mxEvent.isBeingDecrypted() || mxEvent.shouldAttemptDecryption()) {
|
||||
mxEvent.once("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
||||
if (reactions) {
|
||||
reactions.on("Relations.add", this.onReactionsChange);
|
||||
reactions.on("Relations.remove", this.onReactionsChange);
|
||||
reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
mxEvent.off("Event.decrypted", this.onDecrypted);
|
||||
|
||||
if (reactions) {
|
||||
reactions.off("Relations.add", this.onReactionsChange);
|
||||
reactions.off("Relations.remove", this.onReactionsChange);
|
||||
reactions.off("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
|
@ -102,24 +122,12 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
private onDecrypted = () => {
|
||||
// Decryption changes whether the event is actionable
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
private onReactionsChange = () => {
|
||||
// TODO: Call `onHeightChanged` as needed
|
||||
this.setState({
|
||||
myReactions: this.getMyReactions(),
|
||||
|
@ -130,7 +138,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
}
|
||||
|
||||
getMyReactions() {
|
||||
private getMyReactions() {
|
||||
const reactions = this.props.reactions;
|
||||
if (!reactions) {
|
||||
return null;
|
||||
|
@ -143,7 +151,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
|||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onShowAllClick = () => {
|
||||
private onShowAllClick = () => {
|
||||
this.setState({
|
||||
showAll: true,
|
||||
});
|
||||
|
|
|
@ -36,6 +36,7 @@ import {toRightOf} from "../../structures/ContextMenu";
|
|||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
@replaceableComponent("views.messages.TextualBody")
|
||||
export default class TextualBody extends React.Component {
|
||||
|
@ -143,7 +144,7 @@ export default class TextualBody extends React.Component {
|
|||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
|
|
@ -46,6 +46,7 @@ import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
|||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -116,8 +117,8 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
const rect = handle.current.getBoundingClientRect();
|
||||
contextMenu = <WidgetContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={window.innerWidth - rect.right}
|
||||
bottom={window.innerHeight - rect.top}
|
||||
right={UIStore.instance.windowWidth - rect.right}
|
||||
bottom={UIStore.instance.windowHeight - rect.top}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
/>;
|
||||
|
|
|
@ -66,6 +66,7 @@ import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRight
|
|||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -1448,8 +1449,8 @@ const UserInfoHeader: React.FC<{
|
|||
<MemberAvatar
|
||||
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
||||
member={member}
|
||||
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
width={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||
height={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={onMemberAvatarClick}
|
||||
|
|
|
@ -30,6 +30,7 @@ import { Action } from "../../../dispatcher/actions";
|
|||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -65,7 +66,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
contextMenu = (
|
||||
<WidgetContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={window.innerWidth - rect.right - 12}
|
||||
right={UIStore.instance.windowWidth - rect.right - 12}
|
||||
top={rect.bottom + 12}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
|
|
|
@ -36,6 +36,7 @@ import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayout
|
|||
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
||||
import {useStateCallback} from "../../../hooks/useStateCallback";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
@replaceableComponent("views.rooms.AppsDrawer")
|
||||
export default class AppsDrawer extends React.Component {
|
||||
|
@ -290,7 +291,7 @@ const PersistentVResizer = ({
|
|||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
|
||||
if (!maxHeight) maxHeight = (UIStore.instance.windowHeight / 4) * 3;
|
||||
|
||||
// Convert from percentage to height. Note that the default height is 280px.
|
||||
if (defaultHeight) {
|
||||
|
|
|
@ -168,6 +168,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (nextEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: 'edit_event', event: null});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
|
|
@ -790,13 +790,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
return null;
|
||||
}
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
if (!eventId) {
|
||||
// XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120
|
||||
console.error("EventTile attempted to get relations for an event without an ID");
|
||||
// Use event's special `toJSON` method to log key data.
|
||||
console.log(JSON.stringify(this.props.mxEvent, null, 4));
|
||||
console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
|
||||
}
|
||||
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
||||
};
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@ export default class MemberList extends React.Component {
|
|||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
||||
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
});
|
||||
|
@ -252,6 +254,8 @@ export default class MemberList extends React.Component {
|
|||
m.membership === 'join' || m.membership === 'invite'
|
||||
);
|
||||
});
|
||||
const language = SettingsStore.getValue("language");
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
}
|
||||
|
@ -351,13 +355,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
|
||||
const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
|
||||
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
|
||||
return nameA.localeCompare(nameB, {
|
||||
ignorePunctuation: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||
};
|
||||
|
||||
onSearchQueryChanged = searchQuery => {
|
||||
|
@ -422,7 +420,7 @@ export default class MemberList extends React.Component {
|
|||
} else {
|
||||
// Is a 3pid invite
|
||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -484,10 +482,10 @@ export default class MemberList extends React.Component {
|
|||
if (this._getChildCountInvited() > 0) {
|
||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
}
|
||||
|
||||
const footer = (
|
||||
|
@ -520,9 +518,9 @@ export default class MemberList extends React.Component {
|
|||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
|
|
|
@ -55,7 +55,7 @@ interface IProps {
|
|||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
onResize: () => void;
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
isMinimized: boolean;
|
||||
activeSpace: Room;
|
||||
|
@ -404,9 +404,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const newSublists = objectWithOnly(newLists, newListIds);
|
||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||
|
||||
this.setState({sublists, isNameFiltering}, () => {
|
||||
this.props.onResize();
|
||||
});
|
||||
this.setState({sublists, isNameFiltering});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -537,11 +535,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
||||
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
showSkeleton={showSkeleton}
|
||||
extraTiles={extraTiles}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
||||
onListCollapse={this.props.onListCollapse}
|
||||
/>
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,14 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
const RoomListNumResults: React.FC = () => {
|
||||
interface IProps {
|
||||
onVisibilityChange?: () => void
|
||||
}
|
||||
|
||||
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
const [count, setCount] = useState<number>(null);
|
||||
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
|
@ -32,6 +36,12 @@ const RoomListNumResults: React.FC = () => {
|
|||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onVisibilityChange) {
|
||||
onVisibilityChange();
|
||||
}
|
||||
}, [count, onVisibilityChange]);
|
||||
|
||||
if (typeof count !== "number") return null;
|
||||
|
||||
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||
|
|
|
@ -74,11 +74,11 @@ interface IProps {
|
|||
addRoomLabel: string;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
onResize: () => void;
|
||||
showSkeleton?: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
|
||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ interface IState {
|
|||
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
private headerButton = createRef<HTMLDivElement>();
|
||||
private sublistRef = createRef<HTMLDivElement>();
|
||||
private tilesRef = createRef<HTMLDivElement>();
|
||||
private dispatcherRef: string;
|
||||
private layout: ListLayout;
|
||||
private heightAtStart: number;
|
||||
|
@ -246,11 +247,15 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
// Using the passive option to not block the main thread
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||
this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
||||
}
|
||||
|
||||
private onListsUpdated = () => {
|
||||
|
@ -473,7 +478,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private toggleCollapsed = () => {
|
||||
this.layout.isCollapsed = this.state.isExpanded;
|
||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
||||
if (this.props.onListCollapse) {
|
||||
this.props.onListCollapse(!this.layout.isCollapsed)
|
||||
}
|
||||
};
|
||||
|
||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||
|
@ -530,7 +537,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
tiles.push(<RoomTile
|
||||
room={room}
|
||||
key={`room-${room.roomId}`}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showMessagePreview={this.layout.showPreviews}
|
||||
isMinimized={this.props.isMinimized}
|
||||
tag={this.props.tagId}
|
||||
|
@ -754,7 +760,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
|
||||
private onScrollPrevent(e: Event) {
|
||||
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
|
||||
// this fixes https://github.com/vector-im/element-web/issues/14413
|
||||
(e.target as HTMLDivElement).scrollTop = 0;
|
||||
|
@ -883,7 +889,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
className="mx_RoomSublist_resizeBox"
|
||||
enable={handles}
|
||||
>
|
||||
<div className="mx_RoomSublist_tiles" onScroll={this.onScrollPrevent}>
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{visibleTiles}
|
||||
</div>
|
||||
{showNButton}
|
||||
|
|
|
@ -53,14 +53,12 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
isMinimized: boolean;
|
||||
tag: TagID;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||
|
@ -106,9 +104,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
}
|
||||
}
|
||||
|
||||
private countUnsentEvents(): number {
|
||||
|
@ -123,12 +118,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
if (this.showMessagePreview && !this.state.messagePreview) {
|
||||
this.generatePreview();
|
||||
}
|
||||
};
|
||||
|
||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||
if (!room?.roomId === this.props.room.roomId) return;
|
||||
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
|
||||
|
@ -148,7 +137,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
||||
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
|
||||
const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview;
|
||||
const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
|
||||
if (showMessageChanged || minimizedChanged) {
|
||||
this.generatePreview();
|
||||
}
|
||||
if (prevProps.room?.roomId !== this.props.room?.roomId) {
|
||||
|
@ -208,9 +199,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.off("middlePanelResized", this.onResize);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
|
|
|
@ -40,7 +40,7 @@ const STICKERPICKER_Z_INDEX = 3500;
|
|||
const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
||||
|
||||
@replaceableComponent("views.rooms.Stickerpicker")
|
||||
export default class Stickerpicker extends React.Component {
|
||||
export default class Stickerpicker extends React.PureComponent {
|
||||
static currentWidget;
|
||||
|
||||
constructor(props) {
|
||||
|
@ -341,21 +341,27 @@ export default class Stickerpicker extends React.Component {
|
|||
* @param {Event} ev Event that triggered the function call
|
||||
*/
|
||||
_onHideStickersClick(ev) {
|
||||
this.setState({showStickers: false});
|
||||
if (this.state.showStickers) {
|
||||
this.setState({showStickers: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the window is resized
|
||||
*/
|
||||
_onResize() {
|
||||
this.setState({showStickers: false});
|
||||
if (this.state.showStickers) {
|
||||
this.setState({showStickers: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The stickers picker was hidden
|
||||
*/
|
||||
_onFinished() {
|
||||
this.setState({showStickers: false});
|
||||
if (this.state.showStickers) {
|
||||
this.setState({showStickers: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,36 +16,45 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import * as WhoIsTyping from '../../../WhoIsTyping';
|
||||
import Timer from '../../../utils/Timer';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../utils/strings";
|
||||
|
||||
interface IProps {
|
||||
// the room this statusbar is representing.
|
||||
room: Room;
|
||||
onShown?: () => void;
|
||||
onHidden?: () => void;
|
||||
// Number of names to display in typing indication. E.g. set to 3, will
|
||||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: number;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
usersTyping: RoomMember[];
|
||||
// a map with userid => Timer to delay
|
||||
// hiding the "x is typing" message for a
|
||||
// user so hiding it can coincide
|
||||
// with the sent message by the other side
|
||||
// resulting in less timeline jumpiness
|
||||
delayedStopTypingTimers: Record<string, Timer>;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.WhoIsTypingTile")
|
||||
export default class WhoIsTypingTile extends React.Component {
|
||||
static propTypes = {
|
||||
// the room this statusbar is representing.
|
||||
room: PropTypes.object.isRequired,
|
||||
onShown: PropTypes.func,
|
||||
onHidden: PropTypes.func,
|
||||
// Number of names to display in typing indication. E.g. set to 3, will
|
||||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: PropTypes.number,
|
||||
};
|
||||
|
||||
export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
whoIsTypingLimit: 3,
|
||||
};
|
||||
|
||||
state = {
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
// a map with userid => Timer to delay
|
||||
// hiding the "x is typing" message for a
|
||||
// user so hiding it can coincide
|
||||
// with the sent message by the other side
|
||||
// resulting in less timeline jumpiness
|
||||
delayedStopTypingTimers: {},
|
||||
};
|
||||
|
||||
|
@ -71,37 +80,39 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort());
|
||||
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
|
||||
}
|
||||
|
||||
_isVisible(state) {
|
||||
private _isVisible(state: IState): boolean {
|
||||
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
|
||||
}
|
||||
|
||||
isVisible = () => {
|
||||
public isVisible = (): boolean => {
|
||||
return this._isVisible(this.state);
|
||||
};
|
||||
|
||||
onRoomTimeline = (event, room) => {
|
||||
private onRoomTimeline = (event: MatrixEvent, room: Room): void => {
|
||||
if (room?.roomId === this.props.room?.roomId) {
|
||||
const userId = event.getSender();
|
||||
// remove user from usersTyping
|
||||
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
|
||||
this.setState({usersTyping});
|
||||
if (usersTyping.length !== this.state.usersTyping.length) {
|
||||
this.setState({usersTyping});
|
||||
}
|
||||
// abort timer if any
|
||||
this._abortUserTimer(userId);
|
||||
this.abortUserTimer(userId);
|
||||
}
|
||||
};
|
||||
|
||||
onRoomMemberTyping = (ev, member) => {
|
||||
private onRoomMemberTyping = (): void => {
|
||||
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
|
||||
this.setState({
|
||||
delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping),
|
||||
delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping),
|
||||
usersTyping,
|
||||
});
|
||||
};
|
||||
|
||||
_updateDelayedStopTypingTimers(usersTyping) {
|
||||
private updateDelayedStopTypingTimers(usersTyping: RoomMember[]): Record<string, Timer> {
|
||||
const usersThatStoppedTyping = this.state.usersTyping.filter((a) => {
|
||||
return !usersTyping.some((b) => a.userId === b.userId);
|
||||
});
|
||||
|
@ -129,7 +140,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
delayedStopTypingTimers[m.userId] = timer;
|
||||
timer.start();
|
||||
timer.finished().then(
|
||||
() => this._removeUserTimer(m.userId), // on elapsed
|
||||
() => this.removeUserTimer(m.userId), // on elapsed
|
||||
() => {/* aborted */},
|
||||
);
|
||||
}
|
||||
|
@ -139,15 +150,15 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
return delayedStopTypingTimers;
|
||||
}
|
||||
|
||||
_abortUserTimer(userId) {
|
||||
private abortUserTimer(userId: string): void {
|
||||
const timer = this.state.delayedStopTypingTimers[userId];
|
||||
if (timer) {
|
||||
timer.abort();
|
||||
this._removeUserTimer(userId);
|
||||
this.removeUserTimer(userId);
|
||||
}
|
||||
}
|
||||
|
||||
_removeUserTimer(userId) {
|
||||
private removeUserTimer(userId: string): void {
|
||||
const timer = this.state.delayedStopTypingTimers[userId];
|
||||
if (timer) {
|
||||
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
|
||||
|
@ -156,7 +167,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_renderTypingIndicatorAvatars(users, limit) {
|
||||
private renderTypingIndicatorAvatars(users: RoomMember[], limit: number): JSX.Element[] {
|
||||
let othersCount = 0;
|
||||
if (users.length > limit) {
|
||||
othersCount = users.length - limit + 1;
|
||||
|
@ -197,7 +208,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
usersTyping = usersTyping.concat(stoppedUsersOnTimer);
|
||||
// sort them so the typing members don't change order when
|
||||
// moved to delayedStopTypingTimers
|
||||
usersTyping.sort((a, b) => a.name.localeCompare(b.name));
|
||||
usersTyping.sort((a, b) => compare(a.name, b.name));
|
||||
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
usersTyping,
|
||||
|
@ -210,7 +221,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
return (
|
||||
<li className="mx_WhoIsTypingTile" aria-atomic="true">
|
||||
<div className="mx_WhoIsTypingTile_avatars">
|
||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
{ this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
<div className="mx_WhoIsTypingTile_label">
|
||||
{ typingString }
|
|
@ -25,6 +25,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event";
|
|||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
|
||||
const plEventsToLabels = {
|
||||
// These will be translated for us later.
|
||||
|
@ -312,7 +313,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
||||
const comparator = (a, b) => {
|
||||
const plDiff = userLevels[b.key] - userLevels[a.key];
|
||||
return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase());
|
||||
return plDiff !== 0 ? plDiff : compare(a.key.toLocaleLowerCase(), b.key.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
privilegedUsers.sort(comparator);
|
||||
|
|
|
@ -35,9 +35,10 @@ import Field from '../../../elements/Field';
|
|||
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
import {Layout} from "../../../../../settings/Layout";
|
||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { Layout } from "../../../../../settings/Layout";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -295,7 +296,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
.sort((a, b) => compare(a.name, b.name));
|
||||
const orderedThemes = [...builtInThemes, ...customThemes];
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
|
@ -127,6 +127,12 @@ const SpacePanel = () => {
|
|||
const [invites, spaces, activeSpace] = useSpaces();
|
||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPanelCollapsed && menuDisplayed) {
|
||||
closeMenu();
|
||||
}
|
||||
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const newClasses = classNames("mx_SpaceButton_new", {
|
||||
mx_SpaceButton_newCancel: menuDisplayed,
|
||||
});
|
||||
|
@ -235,18 +241,15 @@ const SpacePanel = () => {
|
|||
className={newClasses}
|
||||
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||
onClick={menuDisplayed ? closeMenu : () => {
|
||||
openMenu();
|
||||
if (!isPanelCollapsed) setPanelCollapsed(true);
|
||||
openMenu();
|
||||
}}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
</AutoHideScrollbar>
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
||||
onClick={() => {
|
||||
setPanelCollapsed(!isPanelCollapsed);
|
||||
if (menuDisplayed) closeMenu();
|
||||
}}
|
||||
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
||||
title={expandCollapseButtonTitle}
|
||||
/>
|
||||
{ contextMenu }
|
||||
|
|
|
@ -34,6 +34,7 @@ import { isJoinedOrNearlyJoined } from "./utils/membership";
|
|||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
|
||||
import SpaceStore from "./stores/SpaceStore";
|
||||
import { makeSpaceParentEvent } from "./utils/space";
|
||||
import { Action } from "./dispatcher/actions"
|
||||
|
||||
// we define a number of interfaces which take their names from the js-sdk
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -243,7 +244,8 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
|||
|
||||
// We also failed to join the room (this sets joining to false in RoomViewStore)
|
||||
dis.dispatch({
|
||||
action: 'join_room_error',
|
||||
action: Action.JoinRoomError,
|
||||
roomId,
|
||||
});
|
||||
console.error("Failed to create room " + roomId + " " + err);
|
||||
let description = _t("Server may be unavailable, overloaded, or you hit a bug.");
|
||||
|
|
|
@ -138,4 +138,19 @@ export enum Action {
|
|||
* Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload.
|
||||
*/
|
||||
UploadCanceled = "upload_canceled",
|
||||
|
||||
/**
|
||||
* Fired when requesting to join a room
|
||||
*/
|
||||
JoinRoom = "join_room",
|
||||
|
||||
/**
|
||||
* Fired when successfully joining a room
|
||||
*/
|
||||
JoinRoomReady = "join_room_ready",
|
||||
|
||||
/**
|
||||
* Fired when joining a room failed
|
||||
*/
|
||||
JoinRoomError = "join_room_error",
|
||||
}
|
||||
|
|
|
@ -3284,5 +3284,13 @@
|
|||
"Add reaction": "Přidat reakci",
|
||||
"Send and receive voice messages": "Odeslat a přijmout hlasové zprávy",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Vaše zpětná vazba pomůže zlepšit prostory. Čím podrobnější bude, tím lépe.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Pokud odejdete, %(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné."
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Pokud odejdete, %(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné.",
|
||||
"Space Autocomplete": "Automatické dokončení prostoru",
|
||||
"Go to my space": "Přejít do mého prostoru",
|
||||
"sends space invaders": "pošle space invaders",
|
||||
"Sends the given message with a space themed effect": "Odešle zadanou zprávu s efektem vesmíru",
|
||||
"See when people join, leave, or are invited to your active room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do vaší aktivní místnosti",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Vykopnout, vykázat, pozvat lidi do této místnosti nebo odejít",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Vykopnout, vykázat, pozvat lidi do vaší aktivní místnosti nebo odejít",
|
||||
"See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti"
|
||||
}
|
||||
|
|
|
@ -980,7 +980,7 @@
|
|||
"Enable Emoji suggestions while typing": "Emojivorschläge während Eingabe",
|
||||
"Show a placeholder for removed messages": "Platzhalter für gelöschte Nachrichten",
|
||||
"Show join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Einladungen/Rauswürfe/Banne)",
|
||||
"Show avatar changes": "Avataränderungen anzeigen",
|
||||
"Show avatar changes": "Avataränderungen",
|
||||
"Show display name changes": "Änderungen von Anzeigenamen",
|
||||
"Send typing notifications": "Tippbenachrichtigungen senden",
|
||||
"Show avatars in user and room mentions": "Avatare in Benutzer- und Raumerwähnungen",
|
||||
|
@ -1200,11 +1200,11 @@
|
|||
"Scissors": "Schere",
|
||||
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> zu deiner eigenen Domain",
|
||||
"Accept all %(invitedRooms)s invites": "Akzeptiere alle %(invitedRooms)s Einladungen",
|
||||
"Change room avatar": "Ändere Raumbild",
|
||||
"Change room name": "Ändere Raumname",
|
||||
"Change main address for the room": "Ändere Hauptadresse für den Raum",
|
||||
"Change room avatar": "Raumbild ändern",
|
||||
"Change room name": "Raumname ändern",
|
||||
"Change main address for the room": "Hauptadresse ändern",
|
||||
"Change history visibility": "Sichtbarkeit des Verlaufs ändern",
|
||||
"Change permissions": "Ändere Berechtigungen",
|
||||
"Change permissions": "Berechtigungen ändern",
|
||||
"Change topic": "Thema ändern",
|
||||
"Modify widgets": "Widgets bearbeiten",
|
||||
"Default role": "Standard-Rolle",
|
||||
|
@ -2378,7 +2378,7 @@
|
|||
"A connection error occurred while trying to contact the server.": "Beim Versuch, den Server zu kontaktieren, ist ein Verbindungsfehler aufgetreten.",
|
||||
"You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Du hast sie ggf. in einem anderen Client als %(brand)s konfiguriert. Du kannst sie nicht in %(brand)s verändern, aber sie werden trotzdem angewandt.",
|
||||
"Master private key:": "Privater Hauptschlüssel:",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart & %(brand)s wird versuchen, sie zu verwenden.",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart und %(brand)s wird versuchen, sie zu verwenden.",
|
||||
"Custom Tag": "Benutzerdefinierter Tag",
|
||||
"You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at <a>element.io/get-started</a>.": "Du bist bereits eingeloggt und kannst loslegen. Allerdings kannst du auch die neuesten Versionen der App für alle Plattformen unter <a>element.io/get-started</a> herunterladen.",
|
||||
"You're all caught up.": "Alles gesichtet.",
|
||||
|
@ -2521,7 +2521,7 @@
|
|||
"Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert",
|
||||
"Failed to save your profile": "Speichern des Profils fehlgeschlagen",
|
||||
"The operation could not be completed": "Die Operation konnte nicht abgeschlossen werden",
|
||||
"Remove messages sent by others": "Nachrichten von anderen entfernen",
|
||||
"Remove messages sent by others": "Nachrichten von anderen löschen",
|
||||
"Starting camera...": "Starte Kamera...",
|
||||
"Call connecting...": "Verbinde den Anruf...",
|
||||
"Calling...": "Rufe an...",
|
||||
|
@ -2699,21 +2699,21 @@
|
|||
"Switzerland": "Schweiz",
|
||||
"Sweden": "Schweden",
|
||||
"Swaziland": "Swasiland",
|
||||
"Svalbard & Jan Mayen": "Spitzbergen & Jan Mayen",
|
||||
"Svalbard & Jan Mayen": "Spitzbergen und Jan Mayen",
|
||||
"Suriname": "Surinam",
|
||||
"Sudan": "Sudan",
|
||||
"St. Vincent & Grenadines": "St. Vincent und die Grenadinen",
|
||||
"St. Pierre & Miquelon": "St. Pierre & Miquelon",
|
||||
"St. Pierre & Miquelon": "St. Pierre und Miquelon",
|
||||
"St. Martin": "St. Martin",
|
||||
"St. Lucia": "St. Lucia",
|
||||
"St. Kitts & Nevis": "St. Kitts & Nevis",
|
||||
"St. Kitts & Nevis": "St. Kitts und Nevis",
|
||||
"St. Helena": "St. Helena",
|
||||
"St. Barthélemy": "St. Barthélemy",
|
||||
"Sri Lanka": "Sri Lanka",
|
||||
"Spain": "Spanien",
|
||||
"South Sudan": "Südsudan",
|
||||
"South Korea": "Südkorea",
|
||||
"South Georgia & South Sandwich Islands": "Südgeorgien & Südliche Sandwichinseln",
|
||||
"South Georgia & South Sandwich Islands": "Südgeorgien und Südliche Sandwichinseln",
|
||||
"South Africa": "Südafrika",
|
||||
"Somalia": "Somalia",
|
||||
"Solomon Islands": "Salomonen",
|
||||
|
@ -2815,7 +2815,7 @@
|
|||
"Hungary": "Ungarn",
|
||||
"Hong Kong": "Hongkong",
|
||||
"Honduras": "Honduras",
|
||||
"Heard & McDonald Islands": "Heard & McDonald-Inseln",
|
||||
"Heard & McDonald Islands": "Heard und McDonald-Inseln",
|
||||
"Haiti": "Haiti",
|
||||
"Guyana": "Guyana",
|
||||
"Guinea-Bissau": "Guinea-Bissau",
|
||||
|
@ -2961,9 +2961,9 @@
|
|||
"%(peerName)s held the call": "%(peerName)s hält den Anruf",
|
||||
"You held the call <a>Resume</a>": "Du hältst den Anruf <a>Fortsetzen</a>",
|
||||
"sends fireworks": "sendet Feuerwerk",
|
||||
"Sends the given message with fireworks": "Sendet die gewählte Nachricht mit Feuerwerk",
|
||||
"Sends the given message with fireworks": "Sendet die Nachricht mit Feuerwerk",
|
||||
"sends confetti": "sendet Konfetti",
|
||||
"Sends the given message with confetti": "Sendet die gewählte Nachricht mit Konfetti",
|
||||
"Sends the given message with confetti": "Sendet die Nachricht mit Konfetti",
|
||||
"Show chat effects": "Chat-Effekte anzeigen",
|
||||
"Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Stellt ┬──┬ ノ( ゜-゜ノ) einer Klartextnachricht voran",
|
||||
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Stellt (╯°□°)╯︵ ┻━┻ einer Klartextnachricht voran",
|
||||
|
@ -2976,7 +2976,7 @@
|
|||
"%(name)s on hold": "%(name)s wird gehalten",
|
||||
"You held the call <a>Switch</a>": "Du hältst den Anruf <a>Wechseln</a>",
|
||||
"sends snowfall": "sendet Schneeflocken",
|
||||
"Sends the given message with snowfall": "Sendet die gewählte Nachricht mit Schneeflocken",
|
||||
"Sends the given message with snowfall": "Sendet die Nachricht mit Schneeflocken",
|
||||
"Transfer": "Übertragen",
|
||||
"Failed to transfer call": "Anruf-Übertragung fehlgeschlagen",
|
||||
"A call can only be transferred to a single user.": "Ein Anruf kann nur auf einen einzelnen Nutzer übertragen werden.",
|
||||
|
@ -3089,7 +3089,7 @@
|
|||
"Apply": "Anwenden",
|
||||
"Create a new room": "Neuen Raum erstellen",
|
||||
"Suggested Rooms": "Vorgeschlagene Räume",
|
||||
"Add existing room": "Existierenden Raum",
|
||||
"Add existing room": "Existierenden Raum hinzufügen",
|
||||
"Send message": "Nachricht senden",
|
||||
"New room": "Neuer Raum",
|
||||
"Share invite link": "Einladungslink teilen",
|
||||
|
@ -3253,7 +3253,7 @@
|
|||
"What are some things you want to discuss in %(spaceName)s?": "Welche Themen willst du in %(spaceName)s besprechen?",
|
||||
"Inviting...": "Einladen...",
|
||||
"Failed to create initial space rooms": "Fehler beim Initialisieren des Space",
|
||||
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist die einzige Person hier. Wenn du den Space verlässt, ist er für immer verloren (eine lange Zeit).",
|
||||
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist die einzige Person hier. Wenn du ihn jetzt verlässt, ist er für immer verloren (eine lange Zeit).",
|
||||
"Edit settings relating to your space.": "Einstellungen vom Space bearbeiten.",
|
||||
"Please choose a strong password": "Bitte gib ein sicheres Passwort ein",
|
||||
"If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Wenn du alles zurücksetzt, gehen alle verifizierten Anmeldungen, Benutzer und verschlüsselte Nachrichten verloren.",
|
||||
|
@ -3324,8 +3324,8 @@
|
|||
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Die Betaversion ist für Browser, Desktop und Android verfügbar. Danke, dass Du die Betaversion testest.",
|
||||
"%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s wird mit deaktivierten Spaces neuladen und du kannst Communities und Custom Tags wieder verwenden können.",
|
||||
"Spaces are a beta feature.": "Spaces sind in der Beta.",
|
||||
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt. Um einen existierenden Space beitreten zu können musst du (noch) von jemandem eingeladen werden.",
|
||||
"Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt.",
|
||||
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Wir haben Spaces entwickelt, damit ihr eure Räume besser organisieren könnt. Um einen existierenden Space beitreten zu können musst du (noch) von jemandem eingeladen werden.",
|
||||
"Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure Räume besser organisieren könnt.",
|
||||
"Message search initialisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen",
|
||||
"Send and receive voice messages": "Sprachnachrichten",
|
||||
"Search names and descriptions": "Nach Name und Beschreibung filtern",
|
||||
|
@ -3341,5 +3341,7 @@
|
|||
"Your access token gives full access to your account. Do not share it with anyone.": "Dein Zugriffstoken gibt vollen Zugriff auf dein Konto. Teile es niemals mit jemanden anderen.",
|
||||
"Access Token": "Zugriffstoken",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Dein Feedback hilfst uns, die Spaces zu verbessern. Je genauer, desto besser.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du wieder Communities und Custom Tags verwenden."
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du Communities und Custom Tags wieder verwenden.",
|
||||
"sends space invaders": "sendet Space Invaders",
|
||||
"Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen"
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@
|
|||
"Call Failed": "Call Failed",
|
||||
"Call Declined": "Call Declined",
|
||||
"The other party declined the call.": "The other party declined the call.",
|
||||
"User Busy": "User Busy",
|
||||
"The user you called is busy.": "The user you called is busy.",
|
||||
"The remote side failed to pick up": "The remote side failed to pick up",
|
||||
"The call could not be established": "The call could not be established",
|
||||
"Answered Elsewhere": "Answered Elsewhere",
|
||||
|
@ -2753,6 +2755,8 @@
|
|||
"Switch theme": "Switch theme",
|
||||
"User menu": "User menu",
|
||||
"Community and user menu": "Community and user menu",
|
||||
"Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms",
|
||||
"Currently joining %(count)s rooms|one": "Currently joining %(count)s room",
|
||||
"Could not load user profile": "Could not load user profile",
|
||||
"Decrypted event source": "Decrypted event source",
|
||||
"Original event source": "Original event source",
|
||||
|
|
|
@ -324,7 +324,7 @@
|
|||
"Add rooms to this community": "Aldoni ĉambrojn al ĉi tiu komunumo",
|
||||
"An email has been sent to %(emailAddress)s": "Retletero sendiĝis al %(emailAddress)s",
|
||||
"Please check your email to continue registration.": "Bonvolu kontroli vian retpoŝton por daŭrigi la registriĝon.",
|
||||
"Token incorrect": "Malĝusta ĵetono",
|
||||
"Token incorrect": "Malĝusta peco",
|
||||
"A text message has been sent to %(msisdn)s": "Tekstmesaĝo sendiĝîs al %(msisdn)s",
|
||||
"Please enter the code it contains:": "Bonvolu enigi la enhavatan kodon:",
|
||||
"Start authentication": "Komenci aŭtentikigon",
|
||||
|
@ -769,7 +769,7 @@
|
|||
"Failed to invite users to the room:": "Malsukcesis inviti uzantojn al la ĉambro:",
|
||||
"Opens the Developer Tools dialog": "Maflermas evoluigistan interagujon",
|
||||
"This homeserver has hit its Monthly Active User limit.": "Tiu ĉi hejmservilo atingis sian monatan limon de aktivaj uzantoj.",
|
||||
"This homeserver has exceeded one of its resource limits.": "Tiu ĉi hejmservilo superis je unu el siaj risurcaj limoj.",
|
||||
"This homeserver has exceeded one of its resource limits.": "Tiu ĉi hejmservilo superis je unu el siaj rimedaj limoj.",
|
||||
"Unable to connect to Homeserver. Retrying...": "Ne povas konektiĝi al hejmservilo. Reprovante…",
|
||||
"You do not have permission to invite people to this room.": "Vi ne havas permeson inviti personojn al la ĉambro.",
|
||||
"User %(user_id)s does not exist": "Uzanto %(user_id)s ne ekzistas",
|
||||
|
@ -1569,7 +1569,7 @@
|
|||
"Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Bloki aliĝojn al ĉi tiu ĉambro de uzantoj el aliaj Matrix-serviloj (Ĉi tiun agordon ne eblas poste ŝanĝi!)",
|
||||
"Please fill why you're reporting.": "Bonvolu skribi, kial vi raportas.",
|
||||
"Report Content to Your Homeserver Administrator": "Raporti enhavon al la administrantode via hejmservilo",
|
||||
"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.": "Per raporto de ĉi tiu mesaĝo vi sendos ĝian unikan « eventan identigilon » al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.",
|
||||
"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.": "Per raporto de ĉi tiu mesaĝo vi sendos ĝian unikan «identigilon de okazo» al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.",
|
||||
"Send report": "Sendi raporton",
|
||||
"Command Help": "Helpo pri komando",
|
||||
"To continue you need to accept the terms of this service.": "Por pluigi, vi devas akcepti la uzokondiĉojn de ĉi tiu servo.",
|
||||
|
@ -1653,7 +1653,7 @@
|
|||
"Unencrypted": "Neĉifrita",
|
||||
"Send a reply…": "Sendi respondon…",
|
||||
"Send a message…": "Sendi mesaĝon…",
|
||||
"Direct Messages": "Rektaj ĉambroj",
|
||||
"Direct Messages": "Individuaj ĉambroj",
|
||||
"<userName/> wants to chat": "<userName/> volas babili",
|
||||
"Start chatting": "Ekbabili",
|
||||
"Reject & Ignore user": "Rifuzi kaj malatenti uzanton",
|
||||
|
@ -1665,7 +1665,7 @@
|
|||
"Start Verification": "Komenci kontrolon",
|
||||
"Trusted": "Fidata",
|
||||
"Not trusted": "Nefidata",
|
||||
"Direct message": "Rekta ĉambro",
|
||||
"Direct message": "Individua ĉambro",
|
||||
"Security": "Sekureco",
|
||||
"Reactions": "Reagoj",
|
||||
"More options": "Pliaj elektebloj",
|
||||
|
@ -1919,7 +1919,7 @@
|
|||
"Failed to find the following users": "Malsukcesis trovi la jenajn uzantojn",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "La jenaj uzantoj eble ne ekzistas aŭ ne validas, kaj ne povas invitiĝi: %(csvNames)s",
|
||||
"Recent Conversations": "Freŝaj interparoloj",
|
||||
"Recently Direct Messaged": "Freŝaj rektaj ĉambroj",
|
||||
"Recently Direct Messaged": "Freŝe uzitaj individuaj ĉambroj",
|
||||
"Go": "Iri",
|
||||
"Your account is not secure": "Via konto ne estas sekura",
|
||||
"Your password": "Via pasvorto",
|
||||
|
@ -2312,7 +2312,7 @@
|
|||
"Customise your appearance": "Adaptu vian aspekton",
|
||||
"Appearance Settings only affect this %(brand)s session.": "Agordoj de aspekto nur efikos sur ĉi tiun salutaĵon de %(brand)s.",
|
||||
"Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Aldonu uzantojn kaj servilojn, kiujn vi volas malatenti, ĉi tien. Uzu steletojn por ke %(brand)s atendu iujn ajn signojn. Ekzemple, <code>@bot:*</code> malatentigus ĉiujn uzantojn, kiuj havas la nomon «bot» sur ĉiu ajn servilo.",
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj rektaj ĉambroj.",
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj individuaj ĉambroj.",
|
||||
"Make this room low priority": "Doni al la ĉambro malaltan prioritaton",
|
||||
"Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Ĉambroj kun malalta prioritato montriĝas en aparta sekcio, en la suba parto de via ĉambrobreto,",
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.": "La aŭtentikeco de ĉi tiu ĉifrita mesaĝo ne povas esti garantiita sur ĉi tiu aparato.",
|
||||
|
@ -2391,7 +2391,7 @@
|
|||
"The person who invited you already left the room.": "La persono, kiu vin invitis, jam foriris de la ĉambro.",
|
||||
"The person who invited you already left the room, or their server is offline.": "Aŭ la persono, kiu vin invitis, jam foriris de la ĉambro, aŭ ĝia servilo estas eksterreta.",
|
||||
"Change notification settings": "Ŝanĝi agordojn pri sciigoj",
|
||||
"Show message previews for reactions in DMs": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en rektaj ĉambroj",
|
||||
"Show message previews for reactions in DMs": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en individuaj ĉambroj",
|
||||
"Show message previews for reactions in all rooms": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en ĉiuj ĉambroj",
|
||||
"Your server isn't responding to some <a>requests</a>.": "Via servilo ne respondas al iuj <a>petoj</a>.",
|
||||
"Server isn't responding": "Servilo ne respondas",
|
||||
|
@ -2729,10 +2729,10 @@
|
|||
"Send images as you in your active room": "Sendi bildojn kiel vi en via aktiva ĉambro",
|
||||
"Send images as you in this room": "Sendi bildojn kiel vi en ĉi tiu ĉambro",
|
||||
"The <b>%(capability)s</b> capability": "La kapablo <b>%(capability)s</b>",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "Vidi eventojn de speco <b>%(eventType)s</b> afiŝitajn al via aktiva ĉambro",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "Sendi eventojn de speco <b>%(eventType)s</b> kiel vi en via aktiva ĉambro",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "Vidi eventojn de speco <b>%(eventType)s</b> afiŝitajn al ĉi tiu ĉambro",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "Sendi eventojn de speco <b>%(eventType)s</b> kiel vi en ĉi tiu ĉambro",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "Vidi okazojn de speco <b>%(eventType)s</b> afiŝitajn al via aktiva ĉambro",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "Sendi okazojn de speco <b>%(eventType)s</b> kiel vi en via aktiva ĉambro",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "Vidi okazojn de speco <b>%(eventType)s</b> afiŝitajn al ĉi tiu ĉambro",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "Sendi okazojn de speco <b>%(eventType)s</b> kiel vi en ĉi tiu ĉambro",
|
||||
"See messages posted to your active room": "Vidi mesaĝojn senditajn al via aktiva ĉambro",
|
||||
"See messages posted to this room": "Vidi mesaĝojn senditajn al ĉi tiu ĉambro",
|
||||
"Send messages as you in your active room": "Sendi mesaĝojn kiel vi en via aktiva ĉambro",
|
||||
|
@ -3000,14 +3000,14 @@
|
|||
"Send text messages as you in this room": "Sendi tekstajn mesaĝojn kiel vi en ĉi tiu ĉambro",
|
||||
"Change which room, message, or user you're viewing": "Ŝanĝu, kiun ĉambron, mesaĝon, aŭ uzanton vi rigardas",
|
||||
"%(senderName)s has updated the widget layout": "%(senderName)s ĝisdatigis la aranĝon de la fenestrajoj",
|
||||
"Converts the DM to a room": "Igas la ĉambron nerekta",
|
||||
"Converts the room to a DM": "Igas la ĉambron rekta",
|
||||
"Converts the DM to a room": "Malindividuigas la ĉambron",
|
||||
"Converts the room to a DM": "Individuigas la ĉambron",
|
||||
"Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo rifuzis vian saluton. Eble tio okazis, ĉar ĝi simple daŭris tro longe. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.",
|
||||
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo estis neatingebla kaj ne povis vin salutigi. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.",
|
||||
"Try again": "Reprovu",
|
||||
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Ni petis la foliumilon memori, kiun hejmservilon vi uzas por saluti, sed domaĝe, via foliumilo forgesis. Iru al la saluta paĝo kaj reprovu.",
|
||||
"We couldn't log you in": "Ni ne povis salutigi vin",
|
||||
"%(creator)s created this DM.": "%(creator)s kreis ĉi tiun rektan ĉambron.",
|
||||
"%(creator)s created this DM.": "%(creator)s kreis ĉi tiun individuan ĉambron.",
|
||||
"Invalid URL": "Nevalida URL",
|
||||
"Unable to validate homeserver": "Ne povas validigi hejmservilon",
|
||||
"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>.": "Averte, se vi ne aldonos retpoŝtadreson kaj poste forgesos vian pasvorton, vi eble <b>por ĉiam perdos aliron al via konto</b>.",
|
||||
|
@ -3156,8 +3156,8 @@
|
|||
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Pratipo de Aroj. Malkonforma kun Komunumoj, Komunumoj v2, kaj Propraj etikedoj. Bezonas konforman hejmservilon por iuj funkcioj.",
|
||||
"Verify this login to access your encrypted messages and prove to others that this login is really you.": "Kontrolu ĉi tiun saluton por aliri viajn ĉifritajn mesaĝojn, kaj pruvi al aliuloj, ke la salutanto vere estas vi.",
|
||||
"Verify with another session": "Knotroli per alia salutaĵo",
|
||||
"Original event source": "Originala fonto de evento",
|
||||
"Decrypted event source": "Malĉifrita fonto de evento",
|
||||
"Original event source": "Originala fonto de okazo",
|
||||
"Decrypted event source": "Malĉifrita fonto de okazo",
|
||||
"We'll create rooms for each of them. You can add more later too, including already existing ones.": "Por ĉiu el ili ni kreos ĉambron. Vi povos aldoni pliajn pli poste, inkluzive jam ekzistantajn.",
|
||||
"What projects are you working on?": "Kiujn projektojn vi prilaboras?",
|
||||
"Let's create a room for each of them. You can add more later too, including already existing ones.": "Ni kreu ĉambron por ĉiu el ili. Vi povas aldoni pliajn poste, inkluzive jam ekzistantajn.",
|
||||
|
@ -3218,5 +3218,107 @@
|
|||
"Show options to enable 'Do not disturb' mode": "Montri elekteblojn por ŝalti sendistran reĝimon",
|
||||
"%(deviceId)s from %(ip)s": "%(deviceId)s de %(ip)s",
|
||||
"Review to ensure your account is safe": "Kontrolu por certigi sekurecon de via konto",
|
||||
"Sends the given message as a spoiler": "Sendas la donitan mesaĝon kiel malkaŝon de intrigo"
|
||||
"Sends the given message as a spoiler": "Sendas la donitan mesaĝon kiel malkaŝon de intrigo",
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Ĉu vi certe volas nuligi kreadon de la gastiganto? Ĉi tiu procedo ne estos daŭrigebla.",
|
||||
"Confirm abort of host creation": "Konfirmu nuligon de kreado de gastiganto",
|
||||
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Ĉu vi eksperimentemas? Laboratorioj estas la plej bona maniero frue akiri kaj testi novajn funkciojn, kaj helpi ilin formi antaŭ ilia plena ekuzo. <a>Eksciu plion</a>.",
|
||||
"Your access token gives full access to your account. Do not share it with anyone.": "Via alirpeco donas plenan aliron al via konto. Donu ĝin al neniu.",
|
||||
"We couldn't create your DM.": "Ni ne povis krei vian individuan ĉambron.",
|
||||
"You may contact me if you have any follow up questions": "Vi povas min kontakti okaze de pliaj demandoj",
|
||||
"To leave the beta, visit your settings.": "Por foriri de la prova versio, iru al viaj agordoj.",
|
||||
"Your platform and username will be noted to help us use your feedback as much as we can.": "Via platformo kaj uzantonomo helpos al ni pli bone uzi viajn prikomentojn.",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Viaj prikomentoj helpos plibonigi arojn. Kiom pli detale vi skribos, tiom pli bonos.",
|
||||
"%(featureName)s beta feedback": "Komentoj pri la prova versio de %(featureName)s",
|
||||
"Thank you for your feedback, we really appreciate it.": "Dankon pro viaj prikomentoj, ni vere ilin ŝatas.",
|
||||
"Beta feedback": "Komentoj pri la prova versio",
|
||||
"Want to add a new room instead?": "Ĉu vi volas anstataŭe aldoni novan ĉambron?",
|
||||
"You can add existing spaces to a space.": "Vi povas arigi arojn.",
|
||||
"Feeling experimental?": "Ĉu vi eksperimentemas?",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|one": "Aldonante ĉambron…",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|other": "Aldonante ĉambrojn… (%(progress)s el %(count)s)",
|
||||
"Not all selected were added": "Ne ĉiuj elektitoj aldoniĝis",
|
||||
"You are not allowed to view this server's rooms list": "Vi ne rajtas vidi liston de ĉambroj de tu ĉi servilo",
|
||||
"Add reaction": "Aldoni reagon",
|
||||
"Error processing voice message": "Eraris traktado de voĉmesaĝo",
|
||||
"Delete recording": "Forigi registraĵon",
|
||||
"Stop the recording": "Ĉesigi la registradon",
|
||||
"We didn't find a microphone on your device. Please check your settings and try again.": "Ni ne trovis mikrofonon en via aparato. Bonvolu kontroli viajn agordojn kaj reprovi.",
|
||||
"No microphone found": "Neniu mikrofono troviĝis",
|
||||
"We were unable to access your microphone. Please check your browser settings and try again.": "Ni ne povis aliri vian mikrofonon. Bonvolu kontroli la agordojn de via foliumilo kaj reprovi.",
|
||||
"Unable to access your microphone": "Ne povas aliri vian mikrofonon",
|
||||
"%(count)s results in all spaces|one": "%(count)s rezulto en ĉiuj aroj",
|
||||
"%(count)s results in all spaces|other": "%(count)s rezultoj en ĉiuj aroj",
|
||||
"You have no ignored users.": "Vi malatentas neniujn uzantojn.",
|
||||
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Aroj prezentas novan manieron grupigi ĉambrojn kaj personojn. Por aliĝi al jama spaco, vi bezonos inviton.",
|
||||
"Please enter a name for the space": "Bonvolu enigi nomon por la aro",
|
||||
"Play": "Ludi",
|
||||
"Pause": "Paŭzigi",
|
||||
"Connecting": "Konektante",
|
||||
"Sends the given message with a space themed effect": "Sendas mesaĝon kun la efekto de kosmo",
|
||||
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permesi samtavolajn individuajn vokojn (kaj do videbligi vian IP-adreson al la alia vokanto)",
|
||||
"Send and receive voice messages": "Sendi kaj ricevi voĉmesaĝojn",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Prova versio disponeblas por reto, labortablo, kaj Androido. Iuj funkcioj eble ne disponeblas per via hejmservilo.",
|
||||
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Vi povas forlasi la provan version iam ajn per la agordoj, aŭ per tuŝeto al la prova insigno, kiel tiu ĉi-supre.",
|
||||
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s estos enlegita kun subetno de Aroj. Komunumoj kaj propraj etikedoj iĝos kaŝitaj.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se vi foriros, %(brand)s estos enlegita sen subteno de Aroj. Komunumoj kaj propraj etikedoj ree estos videblaj.",
|
||||
"Spaces are a new way to group rooms and people.": "Aroj prezentas novan manieron grupigi ĉambrojn kaj homojn.",
|
||||
"See when people join, leave, or are invited to your active room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al via aktiva ĉambro",
|
||||
"See when people join, leave, or are invited to this room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al la ĉambro",
|
||||
"This homeserver has been blocked by it's administrator.": "Tiu ĉi hejmservilo estas blokita de sia administranto.",
|
||||
"This homeserver has been blocked by its administrator.": "Tiu ĉi hejmservilo estas blokita de sia administranto.",
|
||||
"Modal Widget": "Reĝima fenestraĵo",
|
||||
"Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Via mesaĝo ne sendiĝis, ĉar ĉi tiu hejmservilo estas blokita de ĝia administranto. Bonvolu <a>kontakti la administranton de via servo</a> por daŭre uzadi la servon.",
|
||||
"Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Elemento por la reto estas eksperimenta sur telefono. Por pli bona sperto kaj freŝaj funkcioj, uzu nian senpagan malfremdan aplikaĵon.",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Forpeli, forbari, aŭ inviti homojn al via aktiva ĉambro, kaj foririgi vin",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Forpeli, forbari, aŭ inviti personojn al la ĉambro, kaj foririgi vin",
|
||||
"Consult first": "Unue konsulti",
|
||||
"Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Provizora daŭrigo permesas al la agorda procedo de %(hostSignupBrand)s aliri vian konton por preni kontrolitajn retpoŝtadresojn. Tiuj ĉi datumoj de konserviĝos.",
|
||||
"Access Token": "Alirpeco",
|
||||
"Message search initialisation failed": "Malsukcesis komenci serĉadon de mesaĝoj",
|
||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Konsultante kun %(transferTarget)s. <a>Transdono al %(transferee)s</a>",
|
||||
"sends space invaders": "sendas imiton de ludo « Space Invaders »",
|
||||
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Prova versio disponeblas por reto, labortablo, kaj Androido. Dankon pro via provo.",
|
||||
"Enter your Security Phrase a second time to confirm it.": "Enigu vian Sekurecan frazon duafoje por ĝin konfirmi.",
|
||||
"Space Autocomplete": "Memaga finfaro de aro",
|
||||
"Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Sen kontrolo, vi ne povos aliri al ĉiuj viaj mesaĝoj, kaj aliuloj vin povos vidi nefidata.",
|
||||
"Verify your identity to access encrypted messages and prove your identity to others.": "Kontrolu vian identecon por aliri ĉifritajn mesaĝojn kaj pruvi vian identecon al aliuloj.",
|
||||
"Use another login": "Uzi alian saluton",
|
||||
"Please choose a strong password": "Bonvolu elekti fortan pasvorton",
|
||||
"You can add more later too, including already existing ones.": "Vi povas aldoni pliajn poste, inkluzive tiujn, kiuj jam ekzistas.",
|
||||
"Let's create a room for each of them.": "Kreu ni ĉambron por ĉiu el ili.",
|
||||
"What are some things you want to discuss in %(spaceName)s?": "Pri kio volus vi diskuti en %(spaceName)s?",
|
||||
"<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.": "<b>Ĉi tio estas prova funkcio.</b> Uzantoj, kiuj nun ricevos inviton, devos ĝin malfermi per <link/> por efektive aliĝi.",
|
||||
"Go to my space": "Iri al mia aro",
|
||||
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elektu aldonotajn ĉambrojn aŭ interparolojn. Ĉi tiu aro estas nur por vi, neniu estos informita. Vi povas aldoni pliajn pli poste.",
|
||||
"What do you want to organise?": "Kion vi volas organizi?",
|
||||
"Skip for now": "Preterpasi ĉi-foje",
|
||||
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "Por aliĝi al %(spaceName)s, ŝaltu <a>la provan version de Aroj</a>",
|
||||
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "Por vidi %(spaceName)s, ŝaltu la <a>provan version de Aroj</a>",
|
||||
"Spaces are a beta feature.": "Aroj estas prova funkcio.",
|
||||
"Search names and descriptions": "Serĉi nomojn kaj priskribojn",
|
||||
"Select a room below first": "Unue elektu ĉambron de sube",
|
||||
"You can select all or individual messages to retry or delete": "Vi povas elekti ĉiujn aŭ unuopajn mesaĝojn, por reprovi aŭ forigi",
|
||||
"Sending": "Sendante",
|
||||
"Retry all": "Reprovi ĉiujn",
|
||||
"Delete all": "Forigi ĉiujn",
|
||||
"Some of your messages have not been sent": "Kelkaj viaj mesaĝoj ne sendiĝis",
|
||||
"Filter all spaces": "Filtri ĉiujn arojn",
|
||||
"Communities are changing to Spaces": "Komunumoj iĝas Aroj",
|
||||
"Verification requested": "Kontrolpeto",
|
||||
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "Vi estas la nura persono tie ĉi. Se vi foriros, neniu alia plu povos aliĝi, inkluzive vin mem.",
|
||||
"Avatar": "Profilbildo",
|
||||
"Join the beta": "Aliĝi al provado",
|
||||
"Leave the beta": "Ĉesi provadon",
|
||||
"Beta": "Prova",
|
||||
"Tap for more info": "Klaku por pliaj informoj",
|
||||
"Spaces is a beta feature": "Aroj estas prova funkcio",
|
||||
"If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Se vi restarigos ĉion, vi rekomencos sen fidataj salutaĵoj, uzantoj, kaj eble ne povos vidi antaŭajn mesaĝojn.",
|
||||
"Only do this if you have no other device to complete verification with.": "Faru tion ĉi nur se vi ne havas alian aparaton, per kiu vi kontrolus ceterajn.",
|
||||
"Forgotten or lost all recovery methods? <a>Reset all</a>": "Ĉu vi forgesis aŭ perdis ĉiujn manierojn de rehavo? <a>Restarigu ĉion</a>",
|
||||
"Reset everything": "Restarigi ĉion",
|
||||
"Verify other login": "Kontroli alian saluton",
|
||||
"Reset event store": "Restarigi deponejon de okazoj",
|
||||
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata",
|
||||
"You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj",
|
||||
"Reset event store?": "Ĉu restarigi deponejon de okazoj?"
|
||||
}
|
||||
|
|
|
@ -192,7 +192,7 @@
|
|||
"Add a topic": "Añadir un tema",
|
||||
"No media permissions": "Sin permisos para el medio",
|
||||
"You may need to manually permit %(brand)s to access your microphone/webcam": "Probablemente necesites dar permisos manualmente a %(brand)s para tu micrófono/cámara",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "¿Salir de la sala «%(roomName)s?",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "¿Salir de la sala «%(roomName)s»?",
|
||||
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "No se puede conectar al servidor base. Por favor, comprueba tu conexión, asegúrate de que el <a>certificado SSL del servidor</a> es de confiaza, y comprueba que no haya extensiones de navegador bloqueando las peticiones.",
|
||||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s eliminó el nombre de la sala.",
|
||||
"Drop File Here": "Deje el fichero aquí",
|
||||
|
@ -1357,7 +1357,7 @@
|
|||
"Compare a unique set of emoji if you don't have a camera on either device": "Comparar un conjunto de iconos si no tienes cámara en ninguno de los dispositivos",
|
||||
"Start": "Empezar",
|
||||
"Waiting for %(displayName)s to verify…": "Esperando la verificación de %(displayName)s…",
|
||||
"Review": "Revise",
|
||||
"Review": "Revisar",
|
||||
"in secret storage": "en almacén secreto",
|
||||
"Secret storage public key:": "Clave pública del almacén secreto:",
|
||||
"in account data": "en datos de cuenta",
|
||||
|
@ -1544,7 +1544,7 @@
|
|||
"Theme added!": "¡Se añadió el tema!",
|
||||
"Custom theme URL": "URL de tema personalizado",
|
||||
"Add theme": "Añadir tema",
|
||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Para informar de un problema de seguridad relacionado con Matrix, por favor lea <a>Security Disclosure Policy</a> de Matrix.or.",
|
||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Para informar de un problema de seguridad relacionado con Matrix, lee la <a>Política de divulgación de seguridad</a> de Matrix.org.",
|
||||
"Keyboard Shortcuts": "Atajos de teclado",
|
||||
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Personaliza tu experiencia con funciones experimentales. <a>Más información</a>.",
|
||||
"Something went wrong. Please try again or view your console for hints.": "Algo salió mal. Por favor, inténtalo de nuevo o mira tu consola para encontrar pistas.",
|
||||
|
@ -2279,8 +2279,8 @@
|
|||
"Create community": "Crear comunidad",
|
||||
"Failed to find the general chat for this community": "No se pudo encontrar el chat general de esta comunidad",
|
||||
"Security & privacy": "Seguridad y privacidad",
|
||||
"All settings": "Todos los ajustes",
|
||||
"Feedback": "Realimentación",
|
||||
"All settings": "Ajustes",
|
||||
"Feedback": "Danos tu opinión",
|
||||
"Community settings": "Configuración de la comunidad",
|
||||
"User settings": "Ajustes de usuario",
|
||||
"Switch to light mode": "Cambiar al tema claro",
|
||||
|
@ -3305,5 +3305,15 @@
|
|||
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "¿Te apetece probar cosas nuevas? Los experimentos son la mejor manera de conseguir acceso anticipado a nuevas funcionalidades, probarlas y ayudar a mejorarlas antes de su lanzamiento. <a>Más información</a>.",
|
||||
"Send and receive voice messages": "Enviar y recibir mensajes de voz",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Tus comentarios ayudarán a mejorar los espacios. Cuanto más detalle incluyas, mejor.",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta disponible para la versión web, de escritorio o Android. Puede que algunas funcionalidades no estén disponibles en tu servidor base."
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta disponible para la versión web, de escritorio o Android. Puede que algunas funcionalidades no estén disponibles en tu servidor base.",
|
||||
"Space Autocomplete": "Autocompletar espacios",
|
||||
"Go to my space": "Ir a mi espacio",
|
||||
"sends space invaders": "enviar space invaders",
|
||||
"Sends the given message with a space themed effect": "Envía un mensaje con efectos espaciales",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si sales, %(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y las etiquetas personalizadas serán visibles de nuevo.",
|
||||
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permitir conexión directa (peer-to-peer) en las llamadas individuales (si lo activas, la otra parte podría ver tu dirección IP)",
|
||||
"See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir",
|
||||
"See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2950,5 +2950,58 @@
|
|||
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Uusi kirjautuminen tilillesi: %(name)s (%(deviceID)s) osoitteesta %(ip)s",
|
||||
"This homeserver has been blocked by its administrator.": "Tämä kotipalvelin on ylläpitäjänsä estämä.",
|
||||
"You're already in a call with this person.": "Olet jo puhelussa tämän henkilön kanssa.",
|
||||
"Already in call": "Olet jo puhelussa"
|
||||
"Already in call": "Olet jo puhelussa",
|
||||
"Please choose a strong password": "Valitse vahva salasana",
|
||||
"You can add more later too, including already existing ones.": "Voit lisätä niitä myöhemmin, mukaan lukien olemassa olevia.",
|
||||
"Let's create a room for each of them.": "Tehdään huone jokaiselle.",
|
||||
"What do you want to organise?": "Mitä haluat järjestää?",
|
||||
"Random": "Satunnainen",
|
||||
"Search names and descriptions": "Etsi nimistä ja kuvauksista",
|
||||
"Failed to remove some rooms. Try again later": "Joitakin huoneita ei voitu poistaa. Yritä myöhemmin uudelleen.",
|
||||
"Select a room below first": "Valitse ensin huone alta",
|
||||
"You can select all or individual messages to retry or delete": "Voit valita kaikki tai yksittäisiä viestejä yritettäväksi uudelleen tai poistettavaksi",
|
||||
"Sending": "Lähetetään",
|
||||
"Retry all": "Yritä kaikkia uudelleen",
|
||||
"Delete all": "Poista kaikki",
|
||||
"Some of your messages have not been sent": "Osaa viesteistäsi ei ole lähetetty",
|
||||
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "Olet ainoa henkilö täällä. Jos lähdet, kukaan ei voi liittyä tulevaisuudessa, et myöskään sinä.",
|
||||
"Beta": "Beeta",
|
||||
"Tap for more info": "Lisää tietoa napauttamalla",
|
||||
"The server is not configured to indicate what the problem is (CORS).": "Palvelinta ei ole säädetty ilmoittamaan, mikä ongelma on kyseessä (CORS).",
|
||||
"Invited people will be able to read old messages.": "Kutsutut ihmiset voivat lukea vanhoja viestejä.",
|
||||
"We couldn't create your DM.": "Yksityisviestiä ei voitu luoda.",
|
||||
"Thank you for your feedback, we really appreciate it.": "Kiitos palautteesta, arvostamme sitä.",
|
||||
"Beta feedback": "Palautetta beetaversiosta",
|
||||
"Want to add a new room instead?": "Haluatko kuitenkin lisätä uuden huoneen?",
|
||||
"Add existing rooms": "Lisää olemassa olevia huoneita",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|one": "Lisätään huonetta...",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|other": "Lisätään huoneita... (%(progress)s out of %(count)s)",
|
||||
"Not all selected were added": "Kaikkia valittuja ei lisätty",
|
||||
"You are not allowed to view this server's rooms list": "Sinulla ei ole oikeuksia nähdä tämän palvelimen huoneluetteloa",
|
||||
"View message": "Näytä viesti",
|
||||
"%(count)s people you know have already joined|one": "%(count)s tuntemasi henkilö on jo liittynyt",
|
||||
"%(count)s people you know have already joined|other": "%(count)s tuntemaasi ihmistä on jo liittynyt",
|
||||
"View all %(count)s members|one": "Näytä yksi jäsen",
|
||||
"View all %(count)s members|other": "Näytä kaikki %(count)s jäsentä",
|
||||
"Add reaction": "Lisää reaktio",
|
||||
"Error processing voice message": "Virhe ääniviestin käsittelyssä",
|
||||
"Delete recording": "Poista äänitys",
|
||||
"Stop the recording": "Lopeta äänitys",
|
||||
"Record a voice message": "Äänitä viesti",
|
||||
"We were unable to access your microphone. Please check your browser settings and try again.": "Mikrofoniasi ei voitu käyttää. Tarkista selaimesi asetukset ja yritä uudelleen.",
|
||||
"We didn't find a microphone on your device. Please check your settings and try again.": "Laitteestasi ei löytynyt mikrofonia. Tarkista asetuksesi ja yritä uudelleen.",
|
||||
"No microphone found": "Mikrofonia ei löytynyt",
|
||||
"Unable to access your microphone": "Mikrofonia ei voi käyttää",
|
||||
"Quick actions": "Pikatoiminnot",
|
||||
"%(seconds)ss left": "%(seconds)s s jäljellä",
|
||||
"Failed to send": "Lähettäminen epäonnistui",
|
||||
"You have no ignored users.": "Et ole sivuuttanut käyttäjiä.",
|
||||
"Warn before quitting": "Varoita ennen lopettamista",
|
||||
"Manage & explore rooms": "Hallitse ja selaa huoneita",
|
||||
"Connecting": "Yhdistetään",
|
||||
"unknown person": "tuntematon henkilö",
|
||||
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Salli vertaisyhteydet 1:1-puheluille (jos otat tämän käyttöön, toinen osapuoli saattaa nähdä IP-osoitteesi)",
|
||||
"Send and receive voice messages": "Lähetä ja vastaanota ääniviestejä",
|
||||
"Show options to enable 'Do not disturb' mode": "Näytä asetukset Älä häiritse -tilan ottamiseksi käyttöön",
|
||||
"%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s"
|
||||
}
|
||||
|
|
|
@ -936,7 +936,7 @@
|
|||
"Failed to load group members": "Échec du chargement des membres du groupe",
|
||||
"Failed to invite users to the room:": "Échec de l’invitation d'utilisateurs dans le salon :",
|
||||
"There was an error joining the room": "Une erreur est survenue en rejoignant le salon",
|
||||
"You do not have permission to invite people to this room.": "Vous n’avez pas la permission d’envoyer des invitations dans ce salon.",
|
||||
"You do not have permission to invite people to this room.": "Vous n’avez pas la permission d’inviter des personnes dans ce salon.",
|
||||
"User %(user_id)s does not exist": "L’utilisateur %(user_id)s n’existe pas",
|
||||
"Unknown server error": "Erreur de serveur inconnue",
|
||||
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Afficher un rappel pour activer la récupération de messages sécurisée dans les salons chiffrés",
|
||||
|
@ -3175,8 +3175,8 @@
|
|||
"Delete": "Supprimer",
|
||||
"Jump to the bottom of the timeline when you send a message": "Sauter en bas du fil de discussion lorsque vous envoyez un message",
|
||||
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Prototype d’espaces. Incompatible avec les communautés, les communautés v2 et les étiquettes personnalisées. Nécessite un serveur d’accueil compatible pour certaines fonctionnalités.",
|
||||
"This homeserver has been blocked by it's administrator.": "Ce serveur d’accueil a été banni par ses administrateurs.",
|
||||
"This homeserver has been blocked by its administrator.": "Ce serveur d’accueil a été banni par ses administrateurs.",
|
||||
"This homeserver has been blocked by it's administrator.": "Ce serveur d’accueil a été bloqué par son administrateur.",
|
||||
"This homeserver has been blocked by its administrator.": "Ce serveur d’accueil a été bloqué par son administrateur.",
|
||||
"You're already in a call with this person.": "Vous êtes déjà en cours d’appel avec cette personne.",
|
||||
"Already in call": "Déjà en cours d’appel",
|
||||
"Space selection": "Sélection d’un espace",
|
||||
|
@ -3344,5 +3344,13 @@
|
|||
"To leave the beta, visit your settings.": "Pour quitter la bêta, consultez les paramètres.",
|
||||
"Your platform and username will be noted to help us use your feedback as much as we can.": "Votre plateforme et nom d’utilisateur seront consignés pour nous aider à tirer le maximum de vos retours.",
|
||||
"Add reaction": "Ajouter une réaction",
|
||||
"Send and receive voice messages": "Envoyer et recevoir des messages vocaux"
|
||||
"Send and receive voice messages": "Envoyer et recevoir des messages vocaux",
|
||||
"See when people join, leave, or are invited to this room": "Voir quand une personne rejoint, quitte ou est invitée sur ce salon",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Exclure, bannir ou inviter une personne dans ce salon et vous permettre de partir",
|
||||
"Space Autocomplete": "Autocomplétion d’espace",
|
||||
"Go to my space": "Aller à mon espace",
|
||||
"sends space invaders": "Envoie les Space Invaders",
|
||||
"Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de l’espace",
|
||||
"See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir"
|
||||
}
|
||||
|
|
|
@ -3367,5 +3367,13 @@
|
|||
"Send and receive voice messages": "Enviar e recibir mensaxes de voz",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "A túa opinión axudaranos a mellorar os espazos. Canto máis detallada sexa moito mellor para nós.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se saes, %(brand)s volverá a cargar con Espazos desactivados. Comunidades e etiquetas personais serán visibles outra vez.",
|
||||
"Message search initialisation failed": "Fallou a inicialización da busca de mensaxes"
|
||||
"Message search initialisation failed": "Fallou a inicialización da busca de mensaxes",
|
||||
"Space Autocomplete": "Autocompletado do espazo",
|
||||
"Go to my space": "Ir ao meu espazo",
|
||||
"sends space invaders": "enviar invasores espaciais",
|
||||
"Sends the given message with a space themed effect": "Envía a mensaxe cun efecto de decorado espacial",
|
||||
"See when people join, leave, or are invited to your active room": "Mira cando alguén se une, sae ou é convidada á túa sala activa",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Expulsa, veta ou convida a persoas á túa sala activa, e fai que saias",
|
||||
"See when people join, leave, or are invited to this room": "Mira cando se une alguén, sae ou é convidada a esta sala",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias"
|
||||
}
|
||||
|
|
|
@ -3359,5 +3359,16 @@
|
|||
"Send and receive voice messages": "Hangüzenet küldése, fogadása",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "A visszajelzése segítség a terek javításához. Minél részletesebb annál jobb.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Távozás után %(brand)s Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek.",
|
||||
"Message search initialisation failed": "Üzenet keresés beállítása sikertelen"
|
||||
"Message search initialisation failed": "Üzenet keresés beállítása sikertelen",
|
||||
"Space Autocomplete": "Tér automatikus kiegészítése",
|
||||
"Go to my space": "Irány a teréhez",
|
||||
"Spaces are a beta feature.": "A terek béta állapotban van.",
|
||||
"Search names and descriptions": "Nevek és leírások keresése",
|
||||
"You may contact me if you have any follow up questions": "Ha további kérdés merülne fel, kapcsolatba léphetnek velem",
|
||||
"sends space invaders": "space invaders küldése",
|
||||
"Sends the given message with a space themed effect": "Üzenet küldése világűrös effekttel",
|
||||
"See when people join, leave, or are invited to your active room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése az aktív szobájában",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket az aktív szobába és, hogy ön elhagyja a szobát",
|
||||
"See when people join, leave, or are invited to this room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése ebben a szobában",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát"
|
||||
}
|
||||
|
|
|
@ -716,5 +716,9 @@
|
|||
"%(duration)sm": "%(duration)sm",
|
||||
"%(duration)ss": "%(duration)ss",
|
||||
"Emoji picker": "Tjáningartáknmyndvalmynd",
|
||||
"Show less": "Sýna minna"
|
||||
"Show less": "Sýna minna",
|
||||
"%(count)s messages deleted.|one": "%(count)s skilaboð eytt.",
|
||||
"%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.",
|
||||
"Message deleted on %(date)s": "Skilaboð eytt á %(date)s",
|
||||
"Message edits": "Skilaboðs breytingar"
|
||||
}
|
||||
|
|
|
@ -3367,5 +3367,13 @@
|
|||
"Send and receive voice messages": "Invia e ricevi messaggi vocali",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "La tua opinione aiuterà a migliorare gli spazi. Più dettagli dai, meglio è.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se esci, %(brand)s si ricaricherà con gli spazi disattivati. Le comunità e le etichette personalizzate saranno di nuovo visibili.",
|
||||
"Message search initialisation failed": "Inizializzazione ricerca messaggi fallita"
|
||||
"Message search initialisation failed": "Inizializzazione ricerca messaggi fallita",
|
||||
"Space Autocomplete": "Autocompletamento spazio",
|
||||
"Go to my space": "Vai nel mio spazio",
|
||||
"sends space invaders": "invia space invaders",
|
||||
"Sends the given message with a space themed effect": "Invia il messaggio con un effetto a tema spaziale",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire",
|
||||
"See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire",
|
||||
"See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva"
|
||||
}
|
||||
|
|
|
@ -351,7 +351,7 @@
|
|||
"Mirror local video feed": "ローカルビデオ映像送信",
|
||||
"Send analytics data": "分析データを送信する",
|
||||
"Enable inline URL previews by default": "デフォルトでインライン URL プレビューを有効にする",
|
||||
"Enable URL previews for this room (only affects you)": "この部屋の URL プレビューを有効にする (あなたにのみ影響する)",
|
||||
"Enable URL previews for this room (only affects you)": "この部屋の URL プレビューを有効にする (あなたにのみ適用)",
|
||||
"Enable URL previews by default for participants in this room": "この部屋の参加者のためにデフォルトで URL プレビューを有効にする",
|
||||
"Room Colour": "部屋の色",
|
||||
"Enable widget screenshots on supported widgets": "サポートされているウィジェットでウィジェットのスクリーンショットを有効にする",
|
||||
|
@ -502,10 +502,10 @@
|
|||
"You have <a>disabled</a> URL previews by default.": "デフォルトで URL プレビューが<a>無効</a>です。",
|
||||
"URL previews are enabled by default for participants in this room.": "この部屋の参加者は、デフォルトで URL プレビューが有効です。",
|
||||
"URL previews are disabled by default for participants in this room.": "この部屋の参加者は、デフォルトで URL プレビューが無効です。",
|
||||
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "このような暗号化された部屋では、URL プレビューはデフォルトで無効になっており、あなたのホームサーバー(プレビューを作成する場所)がこの部屋に表示されているリンクに関する情報を収集できないようにしています。",
|
||||
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "この部屋のように暗号化された部屋では、URL プレビューはデフォルトで無効になっています。あなたのホームサーバー (プレビューを作成する) にこの部屋でやり取りされたリンクの情報を収集されないようにするためです。",
|
||||
"URL Previews": "URL プレビュー",
|
||||
"Historical": "履歴のある",
|
||||
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "メッセージにURLを入力すると、URLプレビューが表示され、タイトル、説明、ウェブサイトからの画像など、そのリンクに関する詳細情報が表示されます。",
|
||||
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "メッセージに URL が含まれる場合、タイトル、説明、ウェブサイトの画像などが URL プレビューとして表示されます。",
|
||||
"Error decrypting audio": "オーディオの復号化エラー",
|
||||
"Error decrypting attachment": "添付ファイルの復号化エラー",
|
||||
"Decrypt %(text)s": "%(text)s を復号",
|
||||
|
@ -753,8 +753,8 @@
|
|||
"Community %(groupId)s not found": "コミュニティ %(groupId)s が見つかりません",
|
||||
"Failed to load %(groupId)s": "%(groupId)s をロードできませんでした",
|
||||
"Failed to reject invitation": "招待を拒否できませんでした",
|
||||
"This room is not public. You will not be able to rejoin without an invite.": "この部屋は公開されていません。 あなたは招待なしで再び参加することはできません。",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "本当にこの部屋「%(roomName)s」から退出してよろしいですか?",
|
||||
"This room is not public. You will not be able to rejoin without an invite.": "この部屋は公開されていません。再度参加するには、招待が必要です。",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "この部屋「%(roomName)s」から退出してよろしいですか?",
|
||||
"Failed to leave room": "部屋からの退出に失敗しました",
|
||||
"Can't leave Server Notices room": "サーバー通知部屋を離れることはできません",
|
||||
"This room is used for important messages from the Homeserver, so you cannot leave it.": "この部屋はホームサーバーからの重要なメッセージに使用されるため、そこを離れることはできません。",
|
||||
|
@ -2406,7 +2406,7 @@
|
|||
"Suggested Rooms": "おすすめの部屋",
|
||||
"Explore space rooms": "スペース内の部屋を探索します",
|
||||
"You do not have permissions to add rooms to this space": "このスペースに部屋を追加する権限がありません",
|
||||
"Add existing room": "既存の部屋を追加します",
|
||||
"Add existing room": "既存の部屋を追加",
|
||||
"You do not have permissions to create new rooms in this space": "このスペースに新しい部屋を作成する権限がありません",
|
||||
"Send message": "メッセージを送ります",
|
||||
"Invite to this space": "このスペースに招待します",
|
||||
|
@ -2417,8 +2417,8 @@
|
|||
"Space options": "スペースのオプション",
|
||||
"Space Home": "スペースのホーム",
|
||||
"New room": "新しい部屋",
|
||||
"Leave space": "スペースを離れる",
|
||||
"Invite people": "人々を招待する",
|
||||
"Leave space": "スペースを退出",
|
||||
"Invite people": "人々を招待",
|
||||
"Share your public space": "公開スペースを共有する",
|
||||
"Invite members": "参加者を招待する",
|
||||
"Invite by email or username": "メールまたはユーザー名で招待する",
|
||||
|
@ -2469,7 +2469,7 @@
|
|||
"Invite to just this room": "この部屋に招待",
|
||||
"Invite to %(spaceName)s": "%(spaceName)s に招待",
|
||||
"Quick actions": "クイックアクション",
|
||||
"A private space for you and your teammates": "",
|
||||
"A private space for you and your teammates": "あなたとチームメイトのプライベートスペース",
|
||||
"Me and my teammates": "自分とチームメイト",
|
||||
"Just me": "自分専用",
|
||||
"Make sure the right people have access to %(name)s": "必要な人が %(name)s にアクセスできるようにします",
|
||||
|
@ -2477,5 +2477,30 @@
|
|||
"Beta": "Beta",
|
||||
"Tap for more info": "タップして詳細を表示",
|
||||
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "スペースは、部屋や人をグループ化する新しい方法です。既存のスペースに参加するには、招待が必要です。",
|
||||
"Check your devices": "デバイスを確認"
|
||||
"Check your devices": "デバイスを確認",
|
||||
"Invite to %(roomName)s": "%(roomName)s へ招待",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta は、ウェブ、デスクトップ、Android で利用可能です。お使いのホームサーバーによっては一部機能が利用できない場合があります。",
|
||||
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s はスペースが有効な状態で再読み込みされます。コミュニティとカスタムタグは非表示になります。",
|
||||
"Communities are changing to Spaces": "コミュニティはスペースに生まれ変わります",
|
||||
"Beta feedback": "Beta フィードバック",
|
||||
"%(featureName)s beta feedback": "%(featureName)s Beta フィードバック",
|
||||
"Send feedback": "フィードバックを送信",
|
||||
"Manage & explore rooms": "部屋の管理および検索",
|
||||
"Select a room below first": "以下から部屋を選択してください",
|
||||
"A private space to organise your rooms": "部屋を整理するためのプライベートスペース",
|
||||
"Private space": "プライベートスペース",
|
||||
"Leave Space": "スペースを退出",
|
||||
"Make this space private": "このスペースを非公開にする",
|
||||
"Welcome %(name)s": "ようこそ、%(name)s",
|
||||
"Are you sure you want to leave the space '%(spaceName)s'?": "このスペース「%(spaceName)s」から退出してよろしいですか?",
|
||||
"This space is not public. You will not be able to rejoin without an invite.": "このスペースは公開されていません。再度参加するには、招待が必要です。",
|
||||
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "この部屋のメンバーはあなただけです。あなたが退出すると、今後あなたを含めて誰もこの部屋に参加できなくなります。",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|one": "部屋を追加中...",
|
||||
"Adding rooms... (%(progress)s out of %(count)s)|other": "部屋を追加中... (%(progress)s / %(count)s)",
|
||||
"Skip for now": "スキップ",
|
||||
"What do you want to organise?": "どれを追加しますか?",
|
||||
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "部屋や会話を追加できます。これはあなた専用のスペースで、他の人からは見えません。後から部屋や会話を追加することもできます。",
|
||||
"Support": "サポート",
|
||||
"You can change these anytime.": "ここで入力した情報はいつでも編集できます。",
|
||||
"Add some details to help people recognise it.": "情報を入力してください。"
|
||||
}
|
||||
|
|
|
@ -171,7 +171,7 @@
|
|||
"Fill screen": "Scherm vullen",
|
||||
"Filter room members": "Gespreksleden filteren",
|
||||
"Forget room": "Gesprek vergeten",
|
||||
"For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie afgemeld. Gelieve u opnieuw aan te melden.",
|
||||
"For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie uitgelogd. Gelieve opnieuw inloggen.",
|
||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s van %(fromPowerLevel)s naar %(toPowerLevel)s",
|
||||
"Guests cannot join this room even if explicitly invited.": "Gasten - zelfs speficiek uitgenodigde - kunnen niet aan dit gesprek deelnemen.",
|
||||
"Hangup": "Ophangen",
|
||||
|
@ -249,7 +249,7 @@
|
|||
"%(senderName)s set a profile picture.": "%(senderName)s heeft een profielfoto ingesteld.",
|
||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s heeft %(displayName)s als weergavenaam aangenomen.",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Tijd in 12-uursformaat tonen (bv. 2:30pm)",
|
||||
"Signed Out": "Afgemeld",
|
||||
"Signed Out": "Uitgelogd",
|
||||
"Sign in": "Inloggen",
|
||||
"Sign out": "Uitloggen",
|
||||
"%(count)s of your messages have not been sent.|other": "Enkele van uw berichten zijn niet verstuurd.",
|
||||
|
@ -350,8 +350,8 @@
|
|||
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Weet u zeker dat u deze gebeurtenis wilt verwijderen? Besef wel dat het verwijderen van een van een gespreksnaams- of onderwerpswijziging die wijziging mogelijk teniet doet.",
|
||||
"Unknown error": "Onbekende fout",
|
||||
"Incorrect password": "Onjuist wachtwoord",
|
||||
"Unable to restore session": "Sessieherstel lukt niet",
|
||||
"If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Als u reeds een recentere versie van %(brand)s heeft gebruikt is uw sessie mogelijk onverenigbaar met deze versie. Sluit dit venster en ga terug naar die recentere versie.",
|
||||
"Unable to restore session": "Herstellen van sessie mislukt",
|
||||
"If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Als u een recentere versie van %(brand)s heeft gebruikt is uw sessie mogelijk niet geschikt voor deze versie. Sluit dit venster en ga terug naar die recentere versie.",
|
||||
"Unknown Address": "Onbekend adres",
|
||||
"ex. @bob:example.com": "bv. @jan:voorbeeld.com",
|
||||
"Add User": "Gebruiker toevoegen",
|
||||
|
@ -802,7 +802,7 @@
|
|||
"Send Logs": "Logs versturen",
|
||||
"Refresh": "Herladen",
|
||||
"We encountered an error trying to restore your previous session.": "Het herstel van uw vorige sessie is mislukt.",
|
||||
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het legen van de opslag van uw browser zal het probleem misschien verhelpen, maar zal u ook uitloggen en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.",
|
||||
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het wissen van de browseropslag zal het probleem misschien verhelpen, maar zal u ook uitloggen en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.",
|
||||
"Collapse Reply Thread": "Reactieketting dichtvouwen",
|
||||
"Can't leave Server Notices room": "Kan servermeldingsgesprek niet verlaten",
|
||||
"This room is used for important messages from the Homeserver, so you cannot leave it.": "Dit gesprek is bedoeld voor belangrijke berichten van de homeserver, dus u kunt het niet verlaten.",
|
||||
|
@ -1203,7 +1203,7 @@
|
|||
"Please <a>contact your service administrator</a> to continue using this service.": "Gelieve <a>contact op te nemen met uw dienstbeheerder</a> om deze dienst te blijven gebruiken.",
|
||||
"Failed to perform homeserver discovery": "Ontdekken van homeserver is mislukt",
|
||||
"Sign in with single sign-on": "Inloggen met eenmalig inloggen",
|
||||
"Create account": "Account aanmaken",
|
||||
"Create account": "Registeren",
|
||||
"Registration has been disabled on this homeserver.": "Registratie is uitgeschakeld op deze homeserver.",
|
||||
"Unable to query for supported registration methods.": "Kan ondersteunde registratiemethoden niet opvragen.",
|
||||
"Create your account": "Maak uw account aan",
|
||||
|
@ -1390,7 +1390,7 @@
|
|||
"Failed to re-authenticate": "Opnieuw inloggen is mislukt",
|
||||
"Enter your password to sign in and regain access to your account.": "Voer uw wachtwoord in om u aan te melden en toegang tot uw account te herkrijgen.",
|
||||
"Forgotten your password?": "Wachtwoord vergeten?",
|
||||
"You're signed out": "U bent afgemeld",
|
||||
"You're signed out": "U bent uitgelogd",
|
||||
"Clear personal data": "Persoonlijke gegevens wissen",
|
||||
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Laat ons weten wat er verkeerd is gegaan, of nog beter, maak een foutrapport aan op GitHub, waarin u het probleem beschrijft.",
|
||||
"Identity Server": "Identiteitsserver",
|
||||
|
@ -1750,7 +1750,7 @@
|
|||
"exists": "aanwezig",
|
||||
"Sign In or Create Account": "Meld u aan of maak een account aan",
|
||||
"Use your account or create a new one to continue.": "Gebruik uw bestaande account of maak een nieuwe aan om verder te gaan.",
|
||||
"Create Account": "Account aanmaken",
|
||||
"Create Account": "Registeren",
|
||||
"Displays information about a user": "Geeft informatie weer over een gebruiker",
|
||||
"Order rooms by name": "Gesprekken sorteren op naam",
|
||||
"Show rooms with unread notifications first": "Gesprekken met ongelezen meldingen eerst tonen",
|
||||
|
@ -2775,7 +2775,7 @@
|
|||
"Attach files from chat or just drag and drop them anywhere in a room.": "Voeg bestanden toe vanuit het gesprek of sleep ze in een gesprek.",
|
||||
"No files visible in this room": "Geen bestanden zichtbaar in dit gesprek",
|
||||
"Sign in with SSO": "Inloggen met SSO",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Gebruik e-mail ook om optioneel ontdekt te worden door bestaande contacten.",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Optioneel kunt u uw e-mail ook gebruiken om ontdekt te worden door al bestaande contacten.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Gebruik e-mail of telefoon om optioneel ontdekt te kunnen worden door bestaande contacten.",
|
||||
"Add an email to be able to reset your password.": "Voeg een e-mail toe om uw wachtwoord te kunnen resetten.",
|
||||
"Forgot password?": "Wachtwoord vergeten?",
|
||||
|
@ -3211,7 +3211,7 @@
|
|||
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "Om %(spaceName)s te bekijken moet u de <a>Spaces beta</a> inschakelen",
|
||||
"Select a room below first": "Start met selecteren van een gesprek hieronder",
|
||||
"Communities are changing to Spaces": "Gemeenschappen worden vervangen door Spaces",
|
||||
"Join the beta": "Aan beta deelnemen",
|
||||
"Join the beta": "Beta inschakelen",
|
||||
"Leave the beta": "Beta verlaten",
|
||||
"Beta": "Beta",
|
||||
"Tap for more info": "Klik voor meer info",
|
||||
|
@ -3235,10 +3235,10 @@
|
|||
"Please enter a name for the space": "Vul een naam in voor deze space",
|
||||
"Connecting": "Verbinden",
|
||||
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Peer-to-peer voor 1op1 oproepen toestaan (als u dit inschakelt kunnen andere personen mogelijk uw ipadres zien)",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta beschikbaar voor web, desktop en Android. Sommige functies zijn nog niet beschikbaar op uw homeserver.",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "De beta is beschikbaar voor web, desktop en Android. Sommige functies zijn nog niet beschikbaar op uw homeserver.",
|
||||
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "U kunt de beta elk moment verlaten via instellingen of door op de beta badge hierboven te klikken.",
|
||||
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s zal herladen met Spaces ingeschakeld. Gemeenschappen en labels worden verborgen.",
|
||||
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta beschikbaar voor web, desktop en Android. Bedankt dat u de beta wilt proberen.",
|
||||
"Beta available for web, desktop and Android. Thank you for trying the beta.": "De beta is beschikbaar voor web, desktop en Android. Bedankt dat u de beta wilt proberen.",
|
||||
"%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s zal herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.",
|
||||
"Spaces are a new way to group rooms and people.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen.",
|
||||
"Message search initialisation failed": "Zoeken in berichten opstarten is mislukt",
|
||||
|
@ -3253,5 +3253,13 @@
|
|||
"Add reaction": "Reactie toevoegen",
|
||||
"Send and receive voice messages": "Stuur en ontvang spraakberichten",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Uw feedback maakt spaces beter. Hoe meer details u kan geven, des te beter.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Als u de pagina nu verlaat zal %(brand)s herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden."
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Als u de pagina nu verlaat zal %(brand)s herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.",
|
||||
"Space Autocomplete": "Space Autocomplete",
|
||||
"Go to my space": "Ga naar mijn space",
|
||||
"sends space invaders": "verstuur space invaders",
|
||||
"Sends the given message with a space themed effect": "Verstuur het bericht met een space-thema-effect",
|
||||
"See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve gesprek",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve gesprek en uzelf laten vertrekken",
|
||||
"See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor dit gesprek",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken"
|
||||
}
|
||||
|
|
|
@ -3356,5 +3356,10 @@
|
|||
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta e gatshme për web, desktop dhe Android. Faleminderit që provoni beta-n.",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Nëse ikni, %(brand)s-i do të ringarkohet me Hapësira të çaktivizuara. Bashkësitë dhe etiketat vetjake do të jenë sërish të dukshme.",
|
||||
"Spaces are a new way to group rooms and people.": "Hapësirat janë një rrugë e re për të grupuar dhoma dhe njerëz.",
|
||||
"Message search initialisation failed": "Dështoi gatitje kërkimi mesazhesh"
|
||||
"Message search initialisation failed": "Dështoi gatitje kërkimi mesazhesh",
|
||||
"Go to my space": "Kalo te hapësira ime",
|
||||
"sends space invaders": "dërgon pushtues hapësire",
|
||||
"Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore",
|
||||
"See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive",
|
||||
"See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë"
|
||||
}
|
||||
|
|
|
@ -3297,5 +3297,13 @@
|
|||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Om du lämnar så kommer %(brand)s att ladda om med utrymmen inaktiverade. Gemenskaper och anpassade taggar kommer att synas igen.",
|
||||
"Spaces are a new way to group rooms and people.": "Utrymmen är nya sätt att gruppera rum och personer.",
|
||||
"<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.": "<b>Det här är en experimentell funktion.</b> För tillfället så behöver nya inbjudna användare öppna inbjudan på <link/> för att faktiskt gå med.",
|
||||
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "För att gå med i %(spaceName)s, aktivera <a>utrymmesbetan</a>"
|
||||
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "För att gå med i %(spaceName)s, aktivera <a>utrymmesbetan</a>",
|
||||
"Space Autocomplete": "Utrymmesautokomplettering",
|
||||
"Go to my space": "Gå till mitt utrymme",
|
||||
"sends space invaders": "skickar Space Invaders",
|
||||
"Sends the given message with a space themed effect": "Skickar det givna meddelandet med en effekt med rymdtema",
|
||||
"See when people join, leave, or are invited to your active room": "Se när folk går med, lämnar eller bjuds in till ditt aktiva rum",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "Kicka, banna eller bjuda in folk till ditt aktiva rum, och tvinga dig att lämna",
|
||||
"See when people join, leave, or are invited to this room": "Se när folk går med, lämnar eller bjuds in till det här rummet",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -169,9 +169,9 @@
|
|||
"No Webcams detected": "未偵測到網路攝影機",
|
||||
"No media permissions": "沒有媒體權限",
|
||||
"You may need to manually permit %(brand)s to access your microphone/webcam": "您可能需要手動允許 %(brand)s 存取您的麥克風/網路攝影機",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "您確定您要想要離開房間 '%(roomName)s' 嗎?",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "你確定你要想要離開房間 '%(roomName)s' 嗎?",
|
||||
"Bans user with given id": "阻擋指定 ID 的使用者",
|
||||
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "無法連線到家伺服器 - 請檢查您的連線,確保您的<a>家伺服器的 SSL 憑證</a>可被信任,而瀏覽器擴充套件也沒有阻擋請求。",
|
||||
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "無法連線到家伺服器 - 請檢查你的連線,確保你的<a>家伺服器的 SSL 憑證</a>可被信任,而瀏覽器擴充套件也沒有阻擋請求。",
|
||||
"%(senderName)s changed their profile picture.": "%(senderName)s 已經變更了他的基本資料圖片。",
|
||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s 變更了 %(powerLevelDiffText)s 權限等級。",
|
||||
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s 將聊天室名稱變更為 %(roomName)s。",
|
||||
|
@ -838,7 +838,7 @@
|
|||
"An error ocurred whilst trying to remove the widget from the room": "嘗試從聊天室移除小工具時發生錯誤",
|
||||
"System Alerts": "系統警告",
|
||||
"Only room administrators will see this warning": "僅聊天室管理員會看到此警告",
|
||||
"Please <a>contact your service administrator</a> to continue using the service.": "請<a>聯絡您的服務管理員</a>以繼續使用服務。",
|
||||
"Please <a>contact your service administrator</a> to continue using the service.": "請<a>聯絡你的服務管理員</a>以繼續使用服務。",
|
||||
"This homeserver has hit its Monthly Active User limit.": "這個主伺服器已經到達其每月活躍使用者限制。",
|
||||
"This homeserver has exceeded one of its resource limits.": "此主伺服器已經超過其中一項資源限制。",
|
||||
"Upgrade Room Version": "更新聊天室版本",
|
||||
|
@ -1160,7 +1160,7 @@
|
|||
"Change": "變更",
|
||||
"Couldn't load page": "無法載入頁面",
|
||||
"This homeserver does not support communities": "此家伺服器不支援社群",
|
||||
"A verification email will be sent to your inbox to confirm setting your new password.": "一封驗證用的電子郵件已經傳送到您的收件匣以確認您設定了新密碼。",
|
||||
"A verification email will be sent to your inbox to confirm setting your new password.": "一封驗證用的電子郵件已經傳送到你的收件匣以確認你設定了新密碼。",
|
||||
"Your password has been reset.": "您的密碼已重設。",
|
||||
"This homeserver does not support login using email address.": "此家伺服器不支援使用電子郵件地址登入。",
|
||||
"Registration has been disabled on this homeserver.": "註冊已在此家伺服器上停用。",
|
||||
|
@ -1973,8 +1973,8 @@
|
|||
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 為此聊天室新增了替代位置 %(addresses)s。",
|
||||
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s 為此聊天室移除了替代位置 %(addresses)s。",
|
||||
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 為此聊天室移除了替代位置 %(addresses)s。",
|
||||
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s 為此聊天是變更了替代位置。",
|
||||
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 為此聊天是變更了主要及替代位置。",
|
||||
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s 為此聊天室變更了替代位置。",
|
||||
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 為此聊天室變更了主要及替代位置。",
|
||||
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "更新聊天室的替代位置時發生錯誤。伺服器可能不允許這麼做,或是昱到了暫時性的故障。",
|
||||
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s 將聊天室名稱從 %(oldRoomName)s 變更為 %(newRoomName)s。",
|
||||
"%(senderName)s changed the addresses for this room.": "%(senderName)s 變更了此聊天室的位置。",
|
||||
|
@ -2889,7 +2889,7 @@
|
|||
"Send messages as you in this room": "在此聊天室以您的身份傳送訊息",
|
||||
"The <b>%(capability)s</b> capability": "<b>%(capability)s</b> 能力",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "檢視發佈到您的活躍聊天室的 <b>%(eventType)s</b> 活動",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "以您的身份在您的活躍聊天是傳送 <b>%(eventType)s</b> 活動",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "以您的身份在您的活躍聊天室傳送 <b>%(eventType)s</b> 活動",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "檢視發佈到此聊天室的 <b>%(eventType)s</b> 活動",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "以您的身份在此聊天室傳送 <b>%(eventType)s</b> 活動",
|
||||
"with state key %(stateKey)s": "帶有狀態金鑰 %(stateKey)s",
|
||||
|
@ -2898,17 +2898,17 @@
|
|||
"Send stickers to your active room as you": "以您的身份傳送貼圖到您活躍的聊天室",
|
||||
"See when a sticker is posted in this room": "檢視貼圖在此聊天室中何時貼出",
|
||||
"Send stickers to this room as you": "以您的身份傳送貼圖到此聊天室",
|
||||
"See when the avatar changes in your active room": "檢視您活躍聊天是的大頭照何時變更",
|
||||
"Change the avatar of your active room": "變更您活躍聊天是的大頭照",
|
||||
"See when the avatar changes in this room": "檢視此聊天是的大頭照何時變更",
|
||||
"See when the avatar changes in your active room": "檢視您活躍聊天室的大頭照何時變更",
|
||||
"Change the avatar of your active room": "變更您活躍聊天室的大頭照",
|
||||
"See when the avatar changes in this room": "檢視此聊天室的大頭照何時變更",
|
||||
"Change the avatar of this room": "變更此聊天室的大頭照",
|
||||
"See when the name changes in your active room": "檢視您活躍聊天室的名稱何時變更",
|
||||
"Change the name of your active room": "變更您活躍聊天室的名稱",
|
||||
"See when the name changes in this room": "檢視此聊天是的名稱何時變更",
|
||||
"See when the name changes in this room": "檢視此聊天室的名稱何時變更",
|
||||
"Change the name of this room": "變更此聊天室的名稱",
|
||||
"See when the topic changes in your active room": "檢視您活躍的聊天是的主題何時變更",
|
||||
"Change the topic of your active room": "變更您活躍聊天是的主題",
|
||||
"See when the topic changes in this room": "檢視此聊天是的主題何時變更",
|
||||
"See when the topic changes in your active room": "檢視您活躍的聊天室的主題何時變更",
|
||||
"Change the topic of your active room": "變更您活躍聊天室的主題",
|
||||
"See when the topic changes in this room": "檢視此聊天室的主題何時變更",
|
||||
"Change the topic of this room": "變更此聊天室的主題",
|
||||
"Change which room you're viewing": "變更您正在檢視的聊天室",
|
||||
"Send stickers into your active room": "傳送貼圖到您活躍的聊天室",
|
||||
|
@ -3226,7 +3226,7 @@
|
|||
"Mark as suggested": "標記為建議",
|
||||
"Mark as not suggested": "標記為不建議",
|
||||
"Removing...": "正在移除……",
|
||||
"Failed to remove some rooms. Try again later": "移除部份聊天是失敗。稍後再試",
|
||||
"Failed to remove some rooms. Try again later": "移除部份聊天室失敗。稍後再試",
|
||||
"%(count)s rooms and 1 space|one": "%(count)s 個聊天室與 1 個空間",
|
||||
"%(count)s rooms and 1 space|other": "%(count)s 個聊天室與 1 個空間",
|
||||
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s 個聊天室與 %(numSpaces)s 個空間",
|
||||
|
@ -3240,7 +3240,7 @@
|
|||
"Open": "開啟",
|
||||
"%(count)s messages deleted.|one": "已刪除 %(count)s 則訊息。",
|
||||
"%(count)s messages deleted.|other": "已刪除 %(count)s 則訊息。",
|
||||
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "這通常只影響伺服器如何處理聊天是。如果您的 %(brand)s 遇到問題,請回報臭蟲。",
|
||||
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "這通常只影響伺服器如何處理聊天室。如果您的 %(brand)s 遇到問題,請回報臭蟲。",
|
||||
"Invite to %(roomName)s": "邀請至 %(roomName)s",
|
||||
"Edit devices": "編輯裝置",
|
||||
"Invite People": "邀請夥伴",
|
||||
|
@ -3370,5 +3370,13 @@
|
|||
"Add reaction": "新增反應",
|
||||
"Send and receive voice messages": "傳送與接收語音訊息",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "您的回饋意見將會讓空間變得更好。您可以輸入愈多細節愈好。",
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "若您離開,%(brand)s 將在停用空間的情況下重新載入。社群與自訂標籤將再次可見。"
|
||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "若您離開,%(brand)s 將在停用空間的情況下重新載入。社群與自訂標籤將再次可見。",
|
||||
"Space Autocomplete": "空間自動完成",
|
||||
"Go to my space": "到我的空間",
|
||||
"sends space invaders": "傳送太空侵略者",
|
||||
"Sends the given message with a space themed effect": "與太空主題效果一起傳送指定的訊息",
|
||||
"See when people join, leave, or are invited to your active room": "檢視人們何時加入、離開或被邀請至您活躍的聊天室",
|
||||
"Kick, ban, or invite people to your active room, and make you leave": "踢除、封鎖或邀請人們到您作用中的聊天室,然後讓您離開",
|
||||
"See when people join, leave, or are invited to this room": "檢視人們何時加入、離開或被邀請至此聊天室",
|
||||
"Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開"
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import WidgetUtils from "../utils/WidgetUtils";
|
|||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import url from 'url';
|
||||
import { compare } from "../utils/strings";
|
||||
|
||||
const KIND_PREFERENCE = [
|
||||
// Ordered: first is most preferred, last is least preferred.
|
||||
|
@ -152,7 +153,7 @@ export class IntegrationManagers {
|
|||
|
||||
if (kind === Kind.Account) {
|
||||
// Order by state_keys (IDs)
|
||||
managers.sort((a, b) => a.id.localeCompare(b.id));
|
||||
managers.sort((a, b) => compare(a.id, b.id));
|
||||
}
|
||||
|
||||
ordered.push(...managers);
|
||||
|
|
|
@ -105,12 +105,14 @@ function safeCounterpartTranslate(text: string, options?: object) {
|
|||
return translated;
|
||||
}
|
||||
|
||||
type SubstitutionValue = number | string | React.ReactNode | ((sub: string) => React.ReactNode);
|
||||
|
||||
export interface IVariables {
|
||||
count?: number;
|
||||
[key: string]: number | string;
|
||||
[key: string]: SubstitutionValue;
|
||||
}
|
||||
|
||||
type Tags = Record<string, (sub: string) => React.ReactNode>;
|
||||
type Tags = Record<string, SubstitutionValue>;
|
||||
|
||||
export type TranslatedString = string | React.ReactNode;
|
||||
|
||||
|
@ -247,7 +249,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri
|
|||
let replaced;
|
||||
// If substitution is a function, call it
|
||||
if (mapping[regexpString] instanceof Function) {
|
||||
replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups);
|
||||
replaced = ((mapping as Tags)[regexpString] as Function)(...capturedGroups);
|
||||
} else {
|
||||
replaced = mapping[regexpString];
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import { _t } from '../languageHandler';
|
|||
import dis from '../dispatcher/dispatcher';
|
||||
import { ISetting, SETTINGS } from "./Settings";
|
||||
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
|
||||
import { WatchManager } from "./WatchManager";
|
||||
import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager";
|
||||
import { SettingLevel } from "./SettingLevel";
|
||||
import SettingsHandler from "./handlers/SettingsHandler";
|
||||
|
||||
|
@ -117,8 +117,8 @@ export default class SettingsStore {
|
|||
// We also maintain a list of monitors which are special watchers: they cause dispatches
|
||||
// when the setting changes. We track which rooms we're monitoring though to ensure we
|
||||
// don't duplicate updates on the bus.
|
||||
private static watchers = {}; // { callbackRef => { callbackFn } }
|
||||
private static monitors = {}; // { settingName => { roomId => callbackRef } }
|
||||
private static watchers = new Map<string, WatchCallbackFn>();
|
||||
private static monitors = new Map<string, Map<string, string>>(); // { settingName => { roomId => callbackRef } }
|
||||
|
||||
// Counter used for generation of watcher IDs
|
||||
private static watcherCount = 1;
|
||||
|
@ -163,7 +163,7 @@ export default class SettingsStore {
|
|||
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
||||
};
|
||||
|
||||
SettingsStore.watchers[watcherId] = localizedCallback;
|
||||
SettingsStore.watchers.set(watcherId, localizedCallback);
|
||||
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
|
||||
|
||||
return watcherId;
|
||||
|
@ -176,13 +176,13 @@ export default class SettingsStore {
|
|||
* to cancel.
|
||||
*/
|
||||
public static unwatchSetting(watcherReference: string) {
|
||||
if (!SettingsStore.watchers[watcherReference]) {
|
||||
if (!SettingsStore.watchers.has(watcherReference)) {
|
||||
console.warn(`Ending non-existent watcher ID ${watcherReference}`);
|
||||
return;
|
||||
}
|
||||
|
||||
defaultWatchManager.unwatchSetting(SettingsStore.watchers[watcherReference]);
|
||||
delete SettingsStore.watchers[watcherReference];
|
||||
defaultWatchManager.unwatchSetting(SettingsStore.watchers.get(watcherReference));
|
||||
SettingsStore.watchers.delete(watcherReference);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,10 +196,10 @@ export default class SettingsStore {
|
|||
public static monitorSetting(settingName: string, roomId: string) {
|
||||
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
|
||||
|
||||
if (!this.monitors[settingName]) this.monitors[settingName] = {};
|
||||
if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map());
|
||||
|
||||
const registerWatcher = () => {
|
||||
this.monitors[settingName][roomId] = SettingsStore.watchSetting(
|
||||
this.monitors.get(settingName).set(roomId, SettingsStore.watchSetting(
|
||||
settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => {
|
||||
dis.dispatch({
|
||||
action: 'setting_updated',
|
||||
|
@ -210,19 +210,20 @@ export default class SettingsStore {
|
|||
newValue,
|
||||
});
|
||||
},
|
||||
);
|
||||
));
|
||||
};
|
||||
|
||||
const hasRoom = Object.keys(this.monitors[settingName]).find((r) => r === roomId || r === null);
|
||||
const rooms = Array.from(this.monitors.get(settingName).keys());
|
||||
const hasRoom = rooms.find((r) => r === roomId || r === null);
|
||||
if (!hasRoom) {
|
||||
registerWatcher();
|
||||
} else {
|
||||
if (roomId === null) {
|
||||
// Unregister all existing watchers and register the new one
|
||||
for (const roomId of Object.keys(this.monitors[settingName])) {
|
||||
SettingsStore.unwatchSetting(this.monitors[settingName][roomId]);
|
||||
}
|
||||
this.monitors[settingName] = {};
|
||||
rooms.forEach(roomId => {
|
||||
SettingsStore.unwatchSetting(this.monitors.get(settingName).get(roomId));
|
||||
});
|
||||
this.monitors.get(settingName).clear();
|
||||
registerWatcher();
|
||||
} // else a watcher is already registered for the room, so don't bother registering it again
|
||||
}
|
||||
|
|
|
@ -18,11 +18,7 @@ import { SettingLevel } from "./SettingLevel";
|
|||
|
||||
export type CallbackFn = (changedInRoomId: string, atLevel: SettingLevel, newValAtLevel: any) => void;
|
||||
|
||||
const IRRELEVANT_ROOM: string = null;
|
||||
|
||||
interface RoomWatcherMap {
|
||||
[roomId: string]: CallbackFn[];
|
||||
}
|
||||
const IRRELEVANT_ROOM = Symbol("irrelevant-room");
|
||||
|
||||
/**
|
||||
* Generalized management class for dealing with watchers on a per-handler (per-level)
|
||||
|
@ -30,25 +26,25 @@ interface RoomWatcherMap {
|
|||
* class, which are then proxied outwards to any applicable watchers.
|
||||
*/
|
||||
export class WatchManager {
|
||||
private watchers: {[settingName: string]: RoomWatcherMap} = {};
|
||||
private watchers = new Map<string, Map<string | symbol, CallbackFn[]>>(); // settingName -> roomId -> CallbackFn[]
|
||||
|
||||
// Proxy for handlers to delegate changes to this manager
|
||||
public watchSetting(settingName: string, roomId: string | null, cb: CallbackFn) {
|
||||
if (!this.watchers[settingName]) this.watchers[settingName] = {};
|
||||
if (!this.watchers[settingName][roomId]) this.watchers[settingName][roomId] = [];
|
||||
this.watchers[settingName][roomId].push(cb);
|
||||
if (!this.watchers.has(settingName)) this.watchers.set(settingName, new Map());
|
||||
if (!this.watchers.get(settingName).has(roomId)) this.watchers.get(settingName).set(roomId, []);
|
||||
this.watchers.get(settingName).get(roomId).push(cb);
|
||||
}
|
||||
|
||||
// Proxy for handlers to delegate changes to this manager
|
||||
public unwatchSetting(cb: CallbackFn) {
|
||||
for (const settingName of Object.keys(this.watchers)) {
|
||||
for (const roomId of Object.keys(this.watchers[settingName])) {
|
||||
this.watchers.forEach((map) => {
|
||||
map.forEach((callbacks) => {
|
||||
let idx;
|
||||
while ((idx = this.watchers[settingName][roomId].indexOf(cb)) !== -1) {
|
||||
this.watchers[settingName][roomId].splice(idx, 1);
|
||||
while ((idx = callbacks.indexOf(cb)) !== -1) {
|
||||
callbacks.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public notifyUpdate(settingName: string, inRoomId: string | null, atLevel: SettingLevel, newValueAtLevel: any) {
|
||||
|
@ -56,21 +52,21 @@ export class WatchManager {
|
|||
// we also don't have a reliable way to get the old value of a setting. Instead, we'll just
|
||||
// let it fall through regardless and let the receiver dedupe if they want to.
|
||||
|
||||
if (!this.watchers[settingName]) return;
|
||||
if (!this.watchers.has(settingName)) return;
|
||||
|
||||
const roomWatchers = this.watchers[settingName];
|
||||
const roomWatchers = this.watchers.get(settingName);
|
||||
const callbacks = [];
|
||||
|
||||
if (inRoomId !== null && roomWatchers[inRoomId]) {
|
||||
callbacks.push(...roomWatchers[inRoomId]);
|
||||
if (inRoomId !== null && roomWatchers.has(inRoomId)) {
|
||||
callbacks.push(...roomWatchers.get(inRoomId));
|
||||
}
|
||||
|
||||
if (!inRoomId) {
|
||||
// Fire updates to all the individual room watchers too, as they probably
|
||||
// care about the change higher up.
|
||||
callbacks.push(...Object.values(roomWatchers).flat(1));
|
||||
} else if (roomWatchers[IRRELEVANT_ROOM]) {
|
||||
callbacks.push(...roomWatchers[IRRELEVANT_ROOM]);
|
||||
// Fire updates to all the individual room watchers too, as they probably care about the change higher up.
|
||||
const callbacks = Array.from(roomWatchers.values()).flat(1);
|
||||
callbacks.push(...callbacks);
|
||||
} else if (roomWatchers.has(IRRELEVANT_ROOM)) {
|
||||
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
|
||||
}
|
||||
|
||||
for (const callback of callbacks) {
|
||||
|
|
|
@ -17,17 +17,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {Store} from 'flux/utils';
|
||||
import {MatrixError} from "matrix-js-sdk/src/http-api";
|
||||
import { Store } from 'flux/utils';
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import * as sdk from '../index';
|
||||
import Modal from '../Modal';
|
||||
import { _t } from '../languageHandler';
|
||||
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
|
||||
import {ActionPayload} from "../dispatcher/payloads";
|
||||
import {retry} from "../utils/promise";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { retry } from "../utils/promise";
|
||||
import CountlyAnalytics from "../CountlyAnalytics";
|
||||
|
||||
const NUM_JOIN_RETRY = 5;
|
||||
|
@ -136,13 +137,13 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
break;
|
||||
// join_room:
|
||||
// - opts: options for joinRoom
|
||||
case 'join_room':
|
||||
case Action.JoinRoom:
|
||||
this.joinRoom(payload);
|
||||
break;
|
||||
case 'join_room_error':
|
||||
case Action.JoinRoomError:
|
||||
this.joinRoomError(payload);
|
||||
break;
|
||||
case 'join_room_ready':
|
||||
case Action.JoinRoomReady:
|
||||
this.setState({ shouldPeek: false });
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
|
@ -217,7 +218,11 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
this.setState(newState);
|
||||
|
||||
if (payload.auto_join) {
|
||||
this.joinRoom(payload);
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: Action.JoinRoom,
|
||||
roomId: payload.room_id,
|
||||
});
|
||||
}
|
||||
} else if (payload.room_alias) {
|
||||
// Try the room alias to room ID navigation cache first to avoid
|
||||
|
@ -298,41 +303,16 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
// We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not
|
||||
// have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
|
||||
// room.
|
||||
dis.dispatch({ action: 'join_room_ready' });
|
||||
dis.dispatch({
|
||||
action: Action.JoinRoomReady,
|
||||
roomId: this.state.roomId,
|
||||
});
|
||||
} catch (err) {
|
||||
dis.dispatch({
|
||||
action: 'join_room_error',
|
||||
action: Action.JoinRoomError,
|
||||
roomId: this.state.roomId,
|
||||
err: err,
|
||||
});
|
||||
|
||||
let msg = err.message ? err.message : JSON.stringify(err);
|
||||
console.log("Failed to join room:", msg);
|
||||
|
||||
if (err.name === "ConnectionError") {
|
||||
msg = _t("There was an error joining the room");
|
||||
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
||||
msg = <div>
|
||||
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
|
||||
{_t("Please contact your homeserver administrator.")}
|
||||
</div>;
|
||||
} else if (err.httpStatus === 404) {
|
||||
const invitingUserId = this.getInvitingUserId(this.state.roomId);
|
||||
// only provide a better error message for invites
|
||||
if (invitingUserId) {
|
||||
// if the inviting user is on the same HS, there can only be one cause: they left.
|
||||
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
|
||||
msg = _t("The person who invited you already left the room.");
|
||||
} else {
|
||||
msg = _t("The person who invited you already left the room, or their server is offline.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
|
||||
title: _t("Failed to join room"),
|
||||
description: msg,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,6 +331,35 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
joining: false,
|
||||
joinError: payload.err,
|
||||
});
|
||||
const err = payload.err;
|
||||
let msg = err.message ? err.message : JSON.stringify(err);
|
||||
console.log("Failed to join room:", msg);
|
||||
|
||||
if (err.name === "ConnectionError") {
|
||||
msg = _t("There was an error joining the room");
|
||||
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
||||
msg = <div>
|
||||
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
|
||||
{_t("Please contact your homeserver administrator.")}
|
||||
</div>;
|
||||
} else if (err.httpStatus === 404) {
|
||||
const invitingUserId = this.getInvitingUserId(this.state.roomId);
|
||||
// only provide a better error message for invites
|
||||
if (invitingUserId) {
|
||||
// if the inviting user is on the same HS, there can only be one cause: they left.
|
||||
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
|
||||
msg = _t("The person who invited you already left the room.");
|
||||
} else {
|
||||
msg = _t("The person who invited you already left the room, or their server is offline.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
|
||||
title: _t("Failed to join room"),
|
||||
description: msg,
|
||||
});
|
||||
}
|
||||
|
||||
public reset() {
|
||||
|
|
112
src/stores/UIStore.ts
Normal file
112
src/stores/UIStore.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
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 EventEmitter from "events";
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import ResizeObserverEntry from 'resize-observer-polyfill/src/ResizeObserverEntry';
|
||||
|
||||
export enum UI_EVENTS {
|
||||
Resize = "resize"
|
||||
}
|
||||
|
||||
export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void;
|
||||
|
||||
export default class UIStore extends EventEmitter {
|
||||
private static _instance: UIStore = null;
|
||||
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
private uiElementDimensions = new Map<string, DOMRectReadOnly>();
|
||||
private trackedUiElements = new Map<Element, string>();
|
||||
|
||||
public windowWidth: number;
|
||||
public windowHeight: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
this.windowWidth = window.innerWidth;
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
this.windowHeight = window.innerHeight;
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
||||
this.resizeObserver.observe(document.body);
|
||||
}
|
||||
|
||||
public static get instance(): UIStore {
|
||||
if (!UIStore._instance) {
|
||||
UIStore._instance = new UIStore();
|
||||
}
|
||||
return UIStore._instance;
|
||||
}
|
||||
|
||||
public static destroy(): void {
|
||||
if (UIStore._instance) {
|
||||
UIStore._instance.resizeObserver.disconnect();
|
||||
UIStore._instance.removeAllListeners();
|
||||
UIStore._instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
public getElementDimensions(name: string): DOMRectReadOnly {
|
||||
return this.uiElementDimensions.get(name);
|
||||
}
|
||||
|
||||
public trackElementDimensions(name: string, element: Element): void {
|
||||
this.trackedUiElements.set(element, name);
|
||||
this.resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
public stopTrackingElementDimensions(name: string): void {
|
||||
let trackedElement: Element;
|
||||
this.trackedUiElements.forEach((trackedElementName, element) => {
|
||||
if (trackedElementName === name) {
|
||||
trackedElement = element;
|
||||
}
|
||||
});
|
||||
if (trackedElement) {
|
||||
this.resizeObserver.unobserve(trackedElement);
|
||||
this.uiElementDimensions.delete(name);
|
||||
this.trackedUiElements.delete(trackedElement);
|
||||
}
|
||||
}
|
||||
|
||||
public isTrackingElementDimensions(name: string): boolean {
|
||||
return this.uiElementDimensions.has(name);
|
||||
}
|
||||
|
||||
private resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
|
||||
const windowEntry = entries.find(entry => entry.target === document.body);
|
||||
|
||||
if (windowEntry) {
|
||||
this.windowWidth = windowEntry.contentRect.width;
|
||||
this.windowHeight = windowEntry.contentRect.height;
|
||||
}
|
||||
|
||||
entries.forEach(entry => {
|
||||
const trackedElementName = this.trackedUiElements.get(entry.target);
|
||||
if (trackedElementName) {
|
||||
this.uiElementDimensions.set(trackedElementName, entry.contentRect);
|
||||
this.emit(trackedElementName, UI_EVENTS.Resize, entry);
|
||||
}
|
||||
});
|
||||
|
||||
this.emit(UI_EVENTS.Resize, entries);
|
||||
}
|
||||
}
|
||||
|
||||
window.mxUIStore = UIStore.instance;
|
|
@ -176,7 +176,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
|
||||
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
||||
const event = payload.event; // TODO: Type out the dispatcher
|
||||
if (!this.previews.has(event.getRoomId())) return; // not important
|
||||
const isHistoricalEvent = payload.hasOwnProperty("isLiveEvent") && !payload.isLiveEvent
|
||||
if (!this.previews.has(event.getRoomId()) || isHistoricalEvent) return; // not important
|
||||
await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -426,8 +426,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return; // don't do anything on rooms that aren't visible
|
||||
}
|
||||
|
||||
if (cause === RoomUpdateCause.NewRoom && !this.prefilterConditions.every(c => c.isVisible(room))) {
|
||||
return; // don't do anything on new rooms which ought not to be shown
|
||||
if ((cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.PossibleTagChange) &&
|
||||
!this.prefilterConditions.every(c => c.isVisible(room))
|
||||
) {
|
||||
return; // don't do anything on new/moved rooms which ought not to be shown
|
||||
}
|
||||
|
||||
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { TagID } from "../../models";
|
||||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { compare } from "../../../../utils/strings";
|
||||
|
||||
/**
|
||||
* Sorts rooms according to the browser's determination of alphabetic.
|
||||
|
@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm";
|
|||
export class AlphabeticAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
return rooms.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
return compare(a.name, b.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
import { compare } from "../../utils/strings";
|
||||
|
||||
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
||||
|
||||
|
@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||
|
||||
if (orderA === orderB) {
|
||||
// We just need a tiebreak
|
||||
return a.id.localeCompare(b.id);
|
||||
return compare(a.id, b.id);
|
||||
}
|
||||
|
||||
return orderA - orderB;
|
||||
|
|
|
@ -74,12 +74,6 @@ export default class ResizeNotifier extends EventEmitter {
|
|||
|
||||
// can be called in quick succession
|
||||
notifyWindowResized() {
|
||||
// no need to throttle this one,
|
||||
// also it could make scrollbars appear for
|
||||
// a split second when the room list manual layout is now
|
||||
// taller than the available space
|
||||
this.emit("leftPanelResized");
|
||||
|
||||
this._updateMiddlePanel();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,24 +51,6 @@ export function defer<T>(): IDeferred<T> {
|
|||
return {resolve, reject, promise};
|
||||
}
|
||||
|
||||
// Promise.allSettled polyfill until browser support is stable in Firefox
|
||||
export function allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>> {
|
||||
if (Promise.allSettled) {
|
||||
return Promise.allSettled<T>(promises);
|
||||
}
|
||||
|
||||
// @ts-ignore - typescript isn't smart enough to see the disjoint here
|
||||
return Promise.all(promises.map((promise) => {
|
||||
return promise.then(value => ({
|
||||
status: "fulfilled",
|
||||
value,
|
||||
})).catch(reason => ({
|
||||
status: "rejected",
|
||||
reason,
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// Helper method to retry a Promise a given number of times or until a predicate fails
|
||||
export async function retry<T, E extends Error>(fn: () => Promise<T>, num: number, predicate?: (e: E) => boolean) {
|
||||
let lastErr: E;
|
||||
|
|
|
@ -73,3 +73,14 @@ export function copyNode(ref: Element): boolean {
|
|||
selectText(ref);
|
||||
return document.execCommand('copy');
|
||||
}
|
||||
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
/**
|
||||
* Performant language-sensitive string comparison
|
||||
* @param a the first string to compare
|
||||
* @param b the second string to compare
|
||||
*/
|
||||
export function compare(a: string, b: string): number {
|
||||
return collator.compare(a, b);
|
||||
}
|
||||
|
|
|
@ -133,7 +133,7 @@ describe('Login', function() {
|
|||
root.setState({
|
||||
flows: [{
|
||||
"type": "m.login.sso",
|
||||
"org.matrix.msc2858.identity_providers": [{
|
||||
"identity_providers": [{
|
||||
id: "a",
|
||||
name: "Provider 1",
|
||||
}, {
|
||||
|
|
|
@ -9,6 +9,8 @@ import sdk from '../../../skinned-sdk';
|
|||
|
||||
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
||||
|
||||
import { compare } from "../../../../src/utils/strings";
|
||||
|
||||
function generateRoomId() {
|
||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||
}
|
||||
|
@ -173,7 +175,7 @@ describe('MemberList', () => {
|
|||
if (!groupChange) {
|
||||
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
||||
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
||||
const nameCompare = nameB.localeCompare(nameA);
|
||||
const nameCompare = compare(nameB, nameA);
|
||||
console.log("Comparing name");
|
||||
expect(nameCompare).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -1325,6 +1325,10 @@
|
|||
"@types/yargs" "^15.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
|
||||
version "3.2.3"
|
||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
|
||||
|
||||
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
|
||||
version "2.1.8-no-fsevents"
|
||||
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b"
|
||||
|
@ -5670,8 +5674,8 @@ mathml-tag-names@^2.1.3:
|
|||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "11.0.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/52a893a8116d60bb76f1b015d3161a15404b3628"
|
||||
version "11.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
another-json "^0.2.0"
|
||||
|
@ -6144,10 +6148,6 @@ object.values@^1.1.1, object.values@^1.1.2:
|
|||
es-abstract "^1.18.0-next.1"
|
||||
has "^1.0.3"
|
||||
|
||||
"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz":
|
||||
version "3.2.1"
|
||||
resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a"
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
|
@ -8438,9 +8438,9 @@ write@1.0.3:
|
|||
mkdirp "^0.5.1"
|
||||
|
||||
ws@^7.2.3:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
|
||||
integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==
|
||||
version "7.4.6"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
||||
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
|
||||
|
||||
xml-name-validator@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
|
Loading…
Reference in a new issue