-
diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx
index c79b5bddd5..15b25ed64b 100644
--- a/src/components/views/rooms/RoomHeader.tsx
+++ b/src/components/views/rooms/RoomHeader.tsx
@@ -29,6 +29,8 @@ import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import { PlaceCallType } from "../../../CallHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Modal from '../../../Modal';
+import InfoDialog from "../dialogs/InfoDialog";
import { throttle } from 'lodash';
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
import { E2EStatus } from '../../../utils/ShieldUtils';
@@ -87,6 +89,14 @@ export default class RoomHeader extends React.Component {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
+ private displayInfoDialogAboutScreensharing() {
+ Modal.createDialog(InfoDialog, {
+ title: _t("Screen sharing is here!"),
+ description: _t("You can now share your screen by pressing the \"screen share\" " +
+ "button during a call. You can even do this in audio calls if both sides support it!"),
+ });
+ }
+
public render() {
let searchStatus = null;
@@ -185,8 +195,8 @@ export default class RoomHeader extends React.Component {
videoCallButton =
this.props.onCallPlaced(
- ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
+ onClick={(ev) => ev.shiftKey ?
+ this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
title={_t("Video call")} />;
}
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 9e7d9ca205..010780565b 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -140,7 +140,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
- showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
+ showCreateNewRoom(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@@ -153,7 +153,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
- showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
+ showAddExistingRooms(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@@ -428,7 +428,9 @@ export default class RoomList extends React.PureComponent {
groupId={g.groupId}
groupName={g.name}
groupAvatarUrl={g.avatarUrl}
- width={32} height={32} resizeMethod='crop'
+ width={32}
+ height={32}
+ resizeMethod='crop'
/>
);
const openGroup = () => {
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index 3cd34b1966..b8a4315e2d 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -536,8 +536,10 @@ export default class RoomPreviewBar extends React.Component {
"If you think you're seeing this message in error, please " +
"submit a bug report .",
{ errcode: this.props.error.errcode },
- { issueLink: label => { label } },
+ { issueLink: label => { label } },
),
];
break;
diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js
index 768a456b35..a2b5566e39 100644
--- a/src/components/views/rooms/SimpleRoomHeader.js
+++ b/src/components/views/rooms/SimpleRoomHeader.js
@@ -35,13 +35,15 @@ export default class SimpleRoomHeader extends React.Component {
let icon;
if (this.props.icon) {
icon = ;
}
return (
-
+
{ icon }
{ this.props.title }
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index c0e6826ba5..6649948331 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -403,8 +403,7 @@ export default class Stickerpicker extends React.PureComponent {
onClick={this._onHideStickersClick}
active={this.state.showStickers.toString()}
title={_t("Hide Stickers")}
- >
- ;
+ />;
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
stickerPicker =
- ;
+ />;
}
return
{ stickersButton }
diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js
index 0f632e7128..d2a3e3a303 100644
--- a/src/components/views/rooms/TopUnreadMessagesBar.js
+++ b/src/components/views/rooms/TopUnreadMessagesBar.js
@@ -32,14 +32,16 @@ export default class TopUnreadMessagesBar extends React.Component {
render() {
return (
-
-
-
+
-
+ onClick={this.props.onCloseClick}
+ />
);
}
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx
index f0df64fcb4..8323320520 100644
--- a/src/components/views/rooms/VoiceRecordComposerTile.tsx
+++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx
@@ -20,7 +20,7 @@ import React, { ReactNode } from "react";
import {
RecordingState,
VoiceRecording,
-} from "../../../voice/VoiceRecording";
+} from "../../../audio/VoiceRecording";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames";
@@ -189,7 +189,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent ;
}
diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx
index 7228e4b939..5dd5ed9ba1 100644
--- a/src/components/views/settings/BridgeTile.tsx
+++ b/src/components/views/settings/BridgeTile.tsx
@@ -124,7 +124,7 @@ export default class BridgeTile extends React.PureComponent {
url={avatarUrl}
/>;
} else {
- networkIcon =
;
+ networkIcon =
;
}
let networkItem = null;
if (network) {
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index 36d5d4aa0c..c3a1544cdc 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -148,13 +148,22 @@ export default class ChangeAvatar extends React.Component {
if (this.props.room && !this.avatarSet) {
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
avatarImg = ;
} else {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
- avatarImg = ;
+ avatarImg = ;
}
let uploadSection;
diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx
index de49c2a980..9966e38de8 100644
--- a/src/components/views/settings/EventIndexPanel.tsx
+++ b/src/components/views/settings/EventIndexPanel.tsx
@@ -178,8 +178,11 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
"appear in search results.",
) }
-
+
{ _t("Enable") }
{ this.state.enabling ? :
}
@@ -203,8 +206,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
brand,
},
{
- nativeLink: sub => { sub } ,
},
) }
@@ -219,8 +224,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
brand,
},
{
- desktopLink: sub =>
{ sub } ,
},
) }
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 02eaaaeea8..d05fca983c 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -172,7 +172,8 @@ export default class ProfileSettings extends React.Component {
>
@@ -181,7 +182,8 @@ export default class ProfileSettings extends React.Component {
{ _t("Profile") }
diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx
index fd8abc0dbe..1f488f1e67 100644
--- a/src/components/views/settings/SetIdServer.tsx
+++ b/src/components/views/settings/SetIdServer.tsx
@@ -426,7 +426,9 @@ export default class SetIdServer extends React.Component
{
disabled={this.state.busy}
forceValidity={this.state.error ? false : null}
/>
- { _t("Change") }
diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index e2f30192b9..b90fb310e0 100644
--- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -97,9 +97,12 @@ export default class GeneralRoomSettingsTab extends React.Component {
{ _t("Room Addresses") }
{ _t("Other") }
{ flairSection }
diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
index edc0220921..9225bc6b94 100644
--- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
@@ -346,8 +346,11 @@ export default class RolesRoomSettingsTab extends React.Component {
let bannedBy = member.events.member.getSender(); // start by falling back to mxid
if (sender) bannedBy = sender.name;
return (
-
);
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
index 88bc2046ce..ede9a5ddb5 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
@@ -15,52 +15,43 @@ limitations under the License.
*/
import React from 'react';
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials";
+import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
-import StyledRadioGroup from '../../../elements/StyledRadioGroup';
+import StyledRadioGroup, { IDefinition } from '../../../elements/StyledRadioGroup';
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
+import AccessibleButton from "../../../elements/AccessibleButton";
+import SpaceStore from "../../../../../stores/SpaceStore";
+import RoomAvatar from "../../../avatars/RoomAvatar";
+import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog';
+import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog';
+import { upgradeRoom } from "../../../../../utils/RoomUpgrade";
+import { arrayHasDiff } from "../../../../../utils/arrays";
import SettingsFlag from '../../../elements/SettingsFlag';
-// Knock and private are reserved keywords which are not yet implemented.
-export enum JoinRule {
- Public = "public",
- Knock = "knock",
- Invite = "invite",
- /**
- * @deprecated Reserved. Should not be used.
- */
- Private = "private",
-}
-
-export enum GuestAccess {
- CanJoin = "can_join",
- Forbidden = "forbidden",
-}
-
-export enum HistoryVisibility {
- Invited = "invited",
- Joined = "joined",
- Shared = "shared",
- WorldReadable = "world_readable",
-}
-
interface IProps {
roomId: string;
}
interface IState {
joinRule: JoinRule;
+ restrictedAllowRoomIds?: string[];
guestAccess: GuestAccess;
history: HistoryVisibility;
hasAliases: boolean;
encrypted: boolean;
+ roomSupportsRestricted?: boolean;
+ preferredRestrictionVersion?: string;
+ showAdvancedSection: boolean;
}
@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
@@ -70,44 +61,58 @@ export default class SecurityRoomSettingsTab extends React.Component(
+ joinRuleEvent,
'join_rule',
JoinRule.Invite,
);
- const guestAccess: GuestAccess = this.pullContentPropertyFromEvent(
- state.getStateEvents("m.room.guest_access", ""),
+ const restrictedAllowRoomIds = joinRule === JoinRule.Restricted
+ ? joinRuleEvent?.getContent().allow
+ ?.filter(a => a.type === RestrictedAllowType.RoomMembership)
+ ?.map(a => a.room_id)
+ : undefined;
+
+ const guestAccess: GuestAccess = this.pullContentPropertyFromEvent(
+ state.getStateEvents(EventType.RoomGuestAccess, ""),
'guest_access',
GuestAccess.Forbidden,
);
- const history: HistoryVisibility = this.pullContentPropertyFromEvent(
- state.getStateEvents("m.room.history_visibility", ""),
+ const history: HistoryVisibility = this.pullContentPropertyFromEvent(
+ state.getStateEvents(EventType.RoomHistoryVisibility, ""),
'history_visibility',
HistoryVisibility.Shared,
);
+
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
- this.setState({ joinRule, guestAccess, history, encrypted });
- const hasAliases = await this.hasAliases();
- this.setState({ hasAliases });
+ const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport;
+ const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support)
+ && restrictedRoomCapabilities.support.includes(room.getVersion());
+ const preferredRestrictionVersion = roomSupportsRestricted ? undefined : restrictedRoomCapabilities?.preferred;
+ this.setState({ joinRule, restrictedAllowRoomIds, guestAccess, history, encrypted,
+ roomSupportsRestricted, preferredRestrictionVersion });
+
+ this.hasAliases().then(hasAliases => this.setState({ hasAliases }));
}
private pullContentPropertyFromEvent(event: MatrixEvent, key: string, defaultValue: T): T {
- if (!event || !event.getContent()) return defaultValue;
- return event.getContent()[key] || defaultValue;
+ return event?.getContent()[key] || defaultValue;
}
componentWillUnmount() {
@@ -115,13 +120,13 @@ export default class SecurityRoomSettingsTab extends React.Component {
- const refreshWhenTypes = [
- 'm.room.join_rules',
- 'm.room.guest_access',
- 'm.room.history_visibility',
- 'm.room.encryption',
+ const refreshWhenTypes: EventType[] = [
+ EventType.RoomJoinRules,
+ EventType.RoomGuestAccess,
+ EventType.RoomHistoryVisibility,
+ EventType.RoomEncryption,
];
- if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
+ if (refreshWhenTypes.includes(e.getType() as EventType)) this.forceUpdate();
};
private onEncryptionChange = () => {
@@ -133,8 +138,10 @@ export default class SecurityRoomSettingsTab extends React.ComponentLearn more about encryption.",
{},
{
- a: sub => { sub } ,
},
),
@@ -147,7 +154,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
console.error(e);
@@ -157,89 +164,91 @@ export default class SecurityRoomSettingsTab extends React.Component {
- e.preventDefault();
- e.stopPropagation();
-
- const joinRule = JoinRule.Invite;
- const guestAccess = GuestAccess.CanJoin;
-
+ private onJoinRuleChange = async (joinRule: JoinRule) => {
const beforeJoinRule = this.state.joinRule;
- const beforeGuestAccess = this.state.guestAccess;
- this.setState({ joinRule, guestAccess });
+
+ let restrictedAllowRoomIds: string[];
+ if (joinRule === JoinRule.Restricted) {
+ const matrixClient = MatrixClientPeg.get();
+ const roomId = this.props.roomId;
+ const room = matrixClient.getRoom(roomId);
+
+ if (beforeJoinRule === JoinRule.Restricted || this.state.roomSupportsRestricted) {
+ // Have the user pick which spaces to allow joins from
+ restrictedAllowRoomIds = await this.editRestrictedRoomIds();
+ if (!Array.isArray(restrictedAllowRoomIds)) return;
+ } else if (this.state.preferredRestrictionVersion) {
+ // Block this action on a room upgrade otherwise it'd make their room unjoinable
+ const targetVersion = this.state.preferredRestrictionVersion;
+ Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
+ roomId,
+ targetVersion,
+ description: _t("This upgrade will allow members of selected spaces " +
+ "access to this room without an invite."),
+ onFinished: (resp) => {
+ if (!resp?.continue) return;
+ upgradeRoom(room, targetVersion, resp.invite);
+ },
+ });
+ return;
+ }
+ }
+
+ if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
+
+ const content: IContent = {
+ join_rule: joinRule,
+ };
+
+ // pre-set the accepted spaces with the currently viewed one as per the microcopy
+ if (joinRule === JoinRule.Restricted) {
+ content.allow = restrictedAllowRoomIds.map(roomId => ({
+ "type": RestrictedAllowType.RoomMembership,
+ "room_id": roomId,
+ }));
+ }
+
+ this.setState({ joinRule, restrictedAllowRoomIds });
const client = MatrixClientPeg.get();
- client.sendStateEvent(
- this.props.roomId,
- "m.room.join_rules",
- { join_rule: joinRule },
- "",
- ).catch((e) => {
+ client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, content, "").catch((e) => {
console.error(e);
- this.setState({ joinRule: beforeJoinRule });
- });
- client.sendStateEvent(
- this.props.roomId,
- "m.room.guest_access",
- { guest_access: guestAccess },
- "",
- ).catch((e) => {
- console.error(e);
- this.setState({ guestAccess: beforeGuestAccess });
+ this.setState({
+ joinRule: beforeJoinRule,
+ restrictedAllowRoomIds: undefined,
+ });
});
};
- private onRoomAccessRadioToggle = (roomAccess: string) => {
- // join_rule
- // INVITE | PUBLIC
- // ----------------------+----------------
- // guest CAN_JOIN | inv_only | pub_with_guest
- // access ----------------------+----------------
- // FORBIDDEN | inv_only | pub_no_guest
- // ----------------------+----------------
-
- // we always set guests can_join here as it makes no sense to have
- // an invite-only room that guests can't join. If you explicitly
- // invite them, you clearly want them to join, whether they're a
- // guest or not. In practice, guest_access should probably have
- // been implemented as part of the join_rules enum.
- let joinRule = JoinRule.Invite;
- let guestAccess = GuestAccess.CanJoin;
-
- switch (roomAccess) {
- case "invite_only":
- // no change - use defaults above
- break;
- case "public_no_guests":
- joinRule = JoinRule.Public;
- guestAccess = GuestAccess.Forbidden;
- break;
- case "public_with_guests":
- joinRule = JoinRule.Public;
- guestAccess = GuestAccess.CanJoin;
- break;
- }
-
- const beforeJoinRule = this.state.joinRule;
- const beforeGuestAccess = this.state.guestAccess;
- this.setState({ joinRule, guestAccess });
+ private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => {
+ const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds;
+ if (!arrayHasDiff(beforeRestrictedAllowRoomIds || [], restrictedAllowRoomIds)) return;
+ this.setState({ restrictedAllowRoomIds });
const client = MatrixClientPeg.get();
- client.sendStateEvent(
- this.props.roomId,
- "m.room.join_rules",
- { join_rule: joinRule },
- "",
- ).catch((e) => {
+ client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, {
+ join_rule: JoinRule.Restricted,
+ allow: restrictedAllowRoomIds.map(roomId => ({
+ "type": RestrictedAllowType.RoomMembership,
+ "room_id": roomId,
+ })),
+ }, "").catch((e) => {
console.error(e);
- this.setState({ joinRule: beforeJoinRule });
+ this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds });
});
- client.sendStateEvent(
- this.props.roomId,
- "m.room.guest_access",
- { guest_access: guestAccess },
- "",
- ).catch((e) => {
+ };
+
+ private onGuestAccessChange = (allowed: boolean) => {
+ const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
+ const beforeGuestAccess = this.state.guestAccess;
+ if (beforeGuestAccess === guestAccess) return;
+
+ this.setState({ guestAccess });
+
+ const client = MatrixClientPeg.get();
+ client.sendStateEvent(this.props.roomId, EventType.RoomGuestAccess, {
+ guest_access: guestAccess,
+ }, "").catch((e) => {
console.error(e);
this.setState({ guestAccess: beforeGuestAccess });
});
@@ -247,8 +256,10 @@ export default class SecurityRoomSettingsTab extends React.Component {
const beforeHistory = this.state.history;
+ if (beforeHistory === history) return;
+
this.setState({ history: history });
- MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", {
+ MatrixClientPeg.get().sendStateEvent(this.props.roomId, EventType.RoomHistoryVisibility, {
history_visibility: history,
}, "").catch((e) => {
console.error(e);
@@ -268,36 +279,48 @@ export default class SecurityRoomSettingsTab extends React.Component (ev.getContent().aliases || []).length > 0);
return hasAliases;
}
}
- private renderRoomAccess() {
+ private editRestrictedRoomIds = async (): Promise => {
+ let selected = this.state.restrictedAllowRoomIds;
+ if (!selected?.length && SpaceStore.instance.activeSpace) {
+ selected = [SpaceStore.instance.activeSpace.roomId];
+ }
+
+ const matrixClient = MatrixClientPeg.get();
+ const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, {
+ matrixClient,
+ room: matrixClient.getRoom(this.props.roomId),
+ selected,
+ }, "mx_ManageRestrictedJoinRuleDialog_wrapper");
+
+ const [restrictedAllowRoomIds] = await finished;
+ return restrictedAllowRoomIds;
+ };
+
+ private onEditRestrictedClick = async () => {
+ const restrictedAllowRoomIds = await this.editRestrictedRoomIds();
+ if (!Array.isArray(restrictedAllowRoomIds)) return;
+ if (restrictedAllowRoomIds.length > 0) {
+ this.onRestrictedRoomIdsChange(restrictedAllowRoomIds);
+ } else {
+ this.onJoinRuleChange(JoinRule.Invite);
+ }
+ };
+
+ private renderJoinRule() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const joinRule = this.state.joinRule;
- const guestAccess = this.state.guestAccess;
- const canChangeAccess = room.currentState.mayClientSendStateEvent("m.room.join_rules", client)
- && room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
-
- let guestWarning = null;
- if (joinRule !== 'public' && guestAccess === 'forbidden') {
- guestWarning = (
-
- );
- }
+ const canChangeJoinRule = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client);
let aliasWarning = null;
- if (joinRule === 'public' && !this.state.hasAliases) {
+ if (joinRule === JoinRule.Public && !this.state.hasAliases) {
aliasWarning = (
@@ -308,34 +331,107 @@ export default class SecurityRoomSettingsTab extends React.Component
[] = [{
+ value: JoinRule.Invite,
+ label: _t("Private (invite only)"),
+ description: _t("Only invited people can join."),
+ checked: this.state.joinRule === JoinRule.Invite
+ || (this.state.joinRule === JoinRule.Restricted && !this.state.restrictedAllowRoomIds?.length),
+ }, {
+ value: JoinRule.Public,
+ label: _t("Public"),
+ description: _t("Anyone can find and join."),
+ }];
+
+ if (this.state.roomSupportsRestricted ||
+ this.state.preferredRestrictionVersion ||
+ joinRule === JoinRule.Restricted
+ ) {
+ let upgradeRequiredPill;
+ if (this.state.preferredRestrictionVersion) {
+ upgradeRequiredPill =
+ { _t("Upgrade required") }
+ ;
+ }
+
+ let description;
+ if (joinRule === JoinRule.Restricted && this.state.restrictedAllowRoomIds?.length) {
+ const shownSpaces = this.state.restrictedAllowRoomIds
+ .map(roomId => client.getRoom(roomId))
+ .filter(room => room?.isSpaceRoom())
+ .slice(0, 4);
+
+ let moreText;
+ if (shownSpaces.length < this.state.restrictedAllowRoomIds.length) {
+ if (shownSpaces.length > 0) {
+ moreText = _t("& %(count)s more", {
+ count: this.state.restrictedAllowRoomIds.length - shownSpaces.length,
+ });
+ } else {
+ moreText = _t("Currently, %(count)s spaces have access", {
+ count: this.state.restrictedAllowRoomIds.length,
+ });
+ }
+ }
+
+ description =
+
+ { _t("Anyone in a space can find and join. Edit which spaces can access here. ", {}, {
+ a: sub =>
+ { sub }
+ ,
+ }) }
+
+
+
+
{ _t("Spaces with access") }
+ { shownSpaces.map(room => {
+ return
+
+ { room.name }
+ ;
+ }) }
+ { moreText && { moreText } }
+
+
;
+ } else if (SpaceStore.instance.activeSpace) {
+ description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", {
+ spaceName: SpaceStore.instance.activeSpace.name,
+ });
+ } else {
+ description = _t("Anyone in a space can find and join. You can select multiple spaces.");
+ }
+
+ radioDefinitions.splice(1, 0, {
+ value: JoinRule.Restricted,
+ label: <>
+ { _t("Space members") }
+ { upgradeRequiredPill }
+ >,
+ description,
+ // if there are 0 allowed spaces then render it as invite only instead
+ checked: this.state.joinRule === JoinRule.Restricted && !!this.state.restrictedAllowRoomIds?.length,
+ });
+ }
+
return (
-
- { guestWarning }
+
+
+ { _t("Decide who can join %(roomName)s.", {
+ roomName: client.getRoom(this.props.roomId)?.name,
+ }) }
+
{ aliasWarning }
);
@@ -345,7 +441,7 @@ export default class SecurityRoomSettingsTab extends React.Component
{
+ this.setState({ showAdvancedSection: !this.state.showAdvancedSection });
+ };
+
+ private renderAdvanced() {
+ const client = MatrixClientPeg.get();
+ const guestAccess = this.state.guestAccess;
+ const state = client.getRoom(this.props.roomId).currentState;
+ const canSetGuestAccess = state.mayClientSendStateEvent(EventType.RoomGuestAccess, client);
+
+ return
+
+
+ { _t("People with supported clients will be able to join " +
+ "the room without having a registered account.") }
+
+
;
+ }
+
render() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const isEncrypted = this.state.encrypted;
- const hasEncryptionPermission = room.currentState.mayClientSendStateEvent("m.room.encryption", client);
+ const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client);
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
let encryptionSettings = null;
@@ -424,18 +544,30 @@ export default class SecurityRoomSettingsTab extends React.Component
{ _t("Once enabled, encryption cannot be disabled.") }
-
{ encryptionSettings }
- { _t("Who can access this room?") }
+ { _t("Access") }
- { this.renderRoomAccess() }
+ { this.renderJoinRule() }
+
+ { this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
+
+ { this.state.showAdvancedSection && this.renderAdvanced() }
+
{ historySection }
);
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
index d1c497b351..44873816dc 100644
--- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
@@ -303,9 +303,12 @@ export default class AppearanceUserSettingsTab extends React.Component
{ _t("Add theme") }
+ >
+ { _t("Add theme") }
+
{ messageElement }
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 2a6e8937a3..238d6cca21 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -426,9 +426,13 @@ export default class GeneralUserSettingsTab extends React.Component {
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
const discoWarning = this.state.requiredPolicyInfo.hasTerms
- ?
+ width="18"
+ height="18"
+ alt={_t("Warning")}
+ />
: null;
let accountManagementSection;
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
index 33de634611..eaf52e6062 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
@@ -134,28 +134,39 @@ export default class HelpUserSettingsTab extends React.Component
{ _t("Credits") }
@@ -254,7 +265,8 @@ export default class HelpUserSettingsTab extends React.Component
"Security Disclosure Policy .", {},
{
a: sub => { sub } ,
},
) }
diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
index aace4ca557..fa854fc4d8 100644
--- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
@@ -86,8 +86,11 @@ export default class LabsUserSettingsTab extends React.Component {
'test out new features and help shape them before they actually launch. ' +
'Learn more .', {}, {
'a': (sub) => {
- return { sub } ;
+ return { sub } ;
},
})
}
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
index 2e5db59d9b..d3da8a7784 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
@@ -26,6 +26,7 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
import SettingsFlag from '../../../elements/SettingsFlag';
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import AccessibleButton from "../../../elements/AccessibleButton";
+import SpaceStore from "../../../../../stores/SpaceStore";
interface IState {
autoLaunch: boolean;
@@ -47,6 +48,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
'breadcrumbs',
];
+ static SPACES_SETTINGS = [
+ "Spaces.allRoomsInHome",
+ ];
+
static KEYBINDINGS_SETTINGS = [
'ctrlFForSearch',
];
@@ -231,6 +236,11 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
+ { SpaceStore.spacesEnabled &&
+ { _t("Spaces") }
+ { this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
+
}
+
{ _t("Keyboard shortcuts") }
diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx
index 6d2cc1f5db..9d3696c5a9 100644
--- a/src/components/views/spaces/SpaceBasicSettings.tsx
+++ b/src/components/views/spaces/SpaceBasicSettings.tsx
@@ -57,11 +57,15 @@ export const SpaceAvatar = ({
src={avatar}
alt=""
/>
- {
- avatarUploadRef.current.value = "";
- setAvatarDataUrl(undefined);
- setAvatar(undefined);
- }} kind="link" className="mx_SpaceBasicSettings_avatar_remove">
+ {
+ avatarUploadRef.current.value = "";
+ setAvatarDataUrl(undefined);
+ setAvatar(undefined);
+ }}
+ kind="link"
+ className="mx_SpaceBasicSettings_avatar_remove"
+ >
{ _t("Delete") }
;
@@ -77,16 +81,21 @@ export const SpaceAvatar = ({
return
{ avatarSection }
- {
- if (!e.target.files?.length) return;
- const file = e.target.files[0];
- setAvatar(file);
- const reader = new FileReader();
- reader.onload = (ev) => {
- setAvatarDataUrl(ev.target.result as string);
- };
- reader.readAsDataURL(file);
- }} accept="image/*" />
+ {
+ if (!e.target.files?.length) return;
+ const file = e.target.files[0];
+ setAvatar(file);
+ const reader = new FileReader();
+ reader.onload = (ev) => {
+ setAvatarDataUrl(ev.target.result as string);
+ };
+ reader.readAsDataURL(file);
+ }}
+ accept="image/*"
+ />
;
};
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 5f16684fb8..406028dbc7 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useContext, useRef, useState } from "react";
+import React, { ComponentProps, RefObject, SyntheticEvent, useContext, useRef, useState } from "react";
import classNames from "classnames";
-import { EventType, RoomType, RoomCreateTypeField } from "matrix-js-sdk/src/@types/event";
+import { RoomType } from "matrix-js-sdk/src/@types/event";
import FocusLock from "react-focus-lock";
import { _t } from "../../../languageHandler";
@@ -24,18 +24,16 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
import createRoom from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import { SpaceAvatar } from "./SpaceBasicSettings";
+import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton";
-import { BetaPill } from "../beta/BetaCard";
-import defaultDispatcher from "../../../dispatcher/dispatcher";
-import { Action } from "../../../dispatcher/actions";
-import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
-import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
-import { Preset } from "matrix-js-sdk/src/@types/partials";
-import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
+import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
import RoomAliasField from "../elements/RoomAliasField";
+import SdkConfig from "../../../SdkConfig";
+import Modal from "../../../Modal";
+import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
+import SettingsStore from "../../../settings/SettingsStore";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
@@ -66,8 +64,111 @@ const nameToAlias = (name: string, domain: string): string => {
return `#${localpart}:${domain}`;
};
-const SpaceCreateMenu = ({ onFinished }) => {
+// XXX: Temporary for the Spaces release only
+export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
+ if (!SdkConfig.get().bug_report_endpoint_url) return null;
+
+ return
+
{ _t("Spaces are a new feature.") }
+
{
+ if (onClick) onClick();
+ Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, {
+ title: _t("Spaces feedback"),
+ subheading: _t("Thank you for trying Spaces. " +
+ "Your feedback will help inform the next versions."),
+ rageshakeLabel: "spaces-feedback",
+ rageshakeData: Object.fromEntries([
+ "feature_spaces.all_rooms",
+ "feature_spaces.space_member_dms",
+ "feature_spaces.space_dm_badges",
+ ].map(k => [k, SettingsStore.getValue(k)])),
+ });
+ }}
+ >
+ { _t("Give feedback.") }
+
+
;
+};
+
+type BProps = Pick, "setAvatar" | "name" | "setName" | "topic" | "setTopic">;
+interface ISpaceCreateFormProps extends BProps {
+ busy: boolean;
+ alias: string;
+ nameFieldRef: RefObject;
+ aliasFieldRef: RefObject;
+ showAliasField?: boolean;
+ onSubmit(e: SyntheticEvent): void;
+ setAlias(alias: string): void;
+}
+
+export const SpaceCreateForm: React.FC = ({
+ busy,
+ onSubmit,
+ setAvatar,
+ name,
+ setName,
+ nameFieldRef,
+ alias,
+ aliasFieldRef,
+ setAlias,
+ showAliasField,
+ topic,
+ setTopic,
+ children,
+}) => {
const cli = useContext(MatrixClientContext);
+ const domain = cli.getDomain();
+
+ return ;
+};
+
+const SpaceCreateMenu = ({ onFinished }) => {
const [visibility, setVisibility] = useState(null);
const [busy, setBusy] = useState(false);
@@ -98,42 +199,26 @@ const SpaceCreateMenu = ({ onFinished }) => {
return;
}
- const initialState: ICreateRoomStateEvent[] = [
- {
- type: EventType.RoomHistoryVisibility,
- content: {
- "history_visibility": visibility === Visibility.Public ? "world_readable" : "invited",
- },
- },
- ];
- if (avatar) {
- const url = await cli.uploadContent(avatar);
-
- initialState.push({
- type: EventType.RoomAvatar,
- content: { url },
- });
- }
-
try {
await createRoom({
createOpts: {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
- creation_content: {
- [RoomCreateTypeField]: RoomType.Space,
- },
- initial_state: initialState,
power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100,
- ...Visibility.Public ? { invite: 0 } : {},
+ ...visibility === Visibility.Public ? { invite: 0 } : {},
},
room_alias_name: visibility === Visibility.Public && alias
? alias.substr(1, alias.indexOf(":") - 1)
: undefined,
topic,
},
+ avatar,
+ roomType: RoomType.Space,
+ historyVisibility: visibility === Visibility.Public
+ ? HistoryVisibility.WorldReadable
+ : HistoryVisibility.Invited,
spinner: false,
encryption: false,
andView: true,
@@ -171,7 +256,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
;
} else {
- const domain = cli.getDomain();
body =
{
}
-
+
{ busy ? _t("Creating...") : _t("Create") }
@@ -252,13 +307,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
managed={false}
>
- {
- onFinished();
- defaultDispatcher.dispatch({
- action: Action.ViewUserSettings,
- initialTabId: UserTab.Labs,
- });
- }} />
{ body }
;
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index 1c4043f150..58e1db4b1d 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -14,115 +14,46 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
-import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
+import React, { ComponentProps, Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
+import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from "../../../languageHandler";
-import RoomAvatar from "../avatars/RoomAvatar";
import { useContextMenu } from "../../structures/ContextMenu";
import SpaceCreateMenu from "./SpaceCreateMenu";
-import { SpaceItem } from "./SpaceTreeLevel";
+import { SpaceButton, SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import { useEventEmitter } from "../../../hooks/useEventEmitter";
+import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore, {
HOME_SPACE,
+ UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
-import NotificationBadge from "../rooms/NotificationBadge";
-import {
- RovingAccessibleButton,
- RovingAccessibleTooltipButton,
- RovingTabIndexProvider,
-} from "../../../accessibility/RovingTabIndex";
+import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
-import { NotificationState } from "../../../stores/notifications/NotificationState";
-
-interface IButtonProps {
- space?: Room;
- className?: string;
- selected?: boolean;
- tooltip?: string;
- notificationState?: NotificationState;
- isNarrow?: boolean;
- onClick(): void;
-}
-
-const SpaceButton: React.FC = ({
- space,
- className,
- selected,
- onClick,
- tooltip,
- notificationState,
- isNarrow,
- children,
-}) => {
- const classes = classNames("mx_SpaceButton", className, {
- mx_SpaceButton_active: selected,
- mx_SpaceButton_narrow: isNarrow,
- });
-
- let avatar = ;
- if (space) {
- avatar = ;
- }
-
- let notifBadge;
- if (notificationState) {
- notifBadge =
- SpaceStore.instance.setActiveRoomInSpace(space)}
- forceCount={false}
- notification={notificationState}
- />
-
;
- }
-
- let button;
- if (isNarrow) {
- button = (
-
-
- { avatar }
- { notifBadge }
- { children }
-
-
- );
- } else {
- button = (
-
-
- { avatar }
- { tooltip }
- { notifBadge }
- { children }
-
-
- );
- }
-
- return
- { button }
- ;
-};
+import SpaceContextMenu from "../context_menus/SpaceContextMenu";
+import IconizedContextMenu, {
+ IconizedContextMenuCheckbox,
+ IconizedContextMenuOptionList,
+} from "../context_menus/IconizedContextMenu";
+import SettingsStore from "../../../settings/SettingsStore";
+import { SettingLevel } from "../../../settings/SettingLevel";
const useSpaces = (): [Room[], Room[], Room | null] => {
- const [invites, setInvites] = useState(SpaceStore.instance.invitedSpaces);
- useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites);
- const [spaces, setSpaces] = useState(SpaceStore.instance.spacePanelSpaces);
- useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
- const [activeSpace, setActiveSpace] = useState(SpaceStore.instance.activeSpace);
- useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
+ const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
+ return SpaceStore.instance.invitedSpaces;
+ });
+ const spaces = useEventEmitterState(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => {
+ return SpaceStore.instance.spacePanelSpaces;
+ });
+ const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
+ return SpaceStore.instance.activeSpace;
+ });
return [invites, spaces, activeSpace];
};
@@ -132,23 +63,108 @@ interface IInnerSpacePanelProps {
setPanelCollapsed: Dispatch>;
}
+const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps) => {
+ const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
+ return SpaceStore.instance.allRoomsInHome;
+ });
+
+ return
+
+ { _t("Home") }
+
+
+ {
+ SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, !allRoomsInHome);
+ }}
+ />
+
+ ;
+};
+
+interface IHomeButtonProps {
+ selected: boolean;
+ isPanelCollapsed: boolean;
+}
+
+const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
+ const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
+ return SpaceStore.instance.allRoomsInHome;
+ });
+
+ return
+ SpaceStore.instance.setActiveSpace(null)}
+ selected={selected}
+ label={allRoomsInHome ? _t("All rooms") : _t("Home")}
+ notificationState={allRoomsInHome
+ ? RoomNotificationStateStore.instance.globalState
+ : SpaceStore.instance.getNotificationState(HOME_SPACE)}
+ isNarrow={isPanelCollapsed}
+ ContextMenuComponent={HomeButtonContextMenu}
+ contextMenuTooltip={_t("Options")}
+ />
+ ;
+};
+
+const CreateSpaceButton = ({
+ isPanelCollapsed,
+ setPanelCollapsed,
+}: Pick) => {
+ // We don't need the handle as we position the menu in a constant location
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
+
+ useEffect(() => {
+ if (!isPanelCollapsed && menuDisplayed) {
+ closeMenu();
+ }
+ }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ let contextMenu = null;
+ if (menuDisplayed) {
+ contextMenu = ;
+ }
+
+ const onNewClick = menuDisplayed ? closeMenu : () => {
+ if (!isPanelCollapsed) setPanelCollapsed(true);
+ openMenu();
+ };
+
+ return
+
+
+ { contextMenu }
+ ;
+};
+
// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => {
const [invites, spaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];
- const homeNotificationState = SpaceStore.spacesTweakAllRoomsEnabled
- ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
-
return
- SpaceStore.instance.setActiveSpace(null)}
- selected={!activeSpace}
- tooltip={SpaceStore.spacesTweakAllRoomsEnabled ? _t("All rooms") : _t("Home")}
- notificationState={homeNotificationState}
- isNarrow={isPanelCollapsed}
- />
+
{ invites.map(s => (
(({ children, isPanelCo
)) }
{ children }
+
;
});
const SpacePanel = () => {
- // We don't need the handle as we position the menu in a constant location
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
- useEffect(() => {
- if (!isPanelCollapsed && menuDisplayed) {
- closeMenu();
- }
- }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
-
- let contextMenu = null;
- if (menuDisplayed) {
- contextMenu = ;
- }
-
const onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
@@ -259,11 +262,6 @@ const SpacePanel = () => {
}
};
- const onNewClick = menuDisplayed ? closeMenu : () => {
- if (!isPanelCollapsed) setPanelCollapsed(true);
- openMenu();
- };
-
return (
{
if (!result.destination) return; // dropped outside the list
@@ -291,15 +289,6 @@ const SpacePanel = () => {
>
{ provided.placeholder }
-
-
) }
@@ -308,7 +297,6 @@ const SpacePanel = () => {
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
/>
- { contextMenu }
) }
diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
index a43b180752..595bdb2448 100644
--- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
+++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
@@ -21,12 +21,11 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
-import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import SpaceBasicSettings from "./SpaceBasicSettings";
import { avatarUrlForRoom } from "../../../Avatar";
import { IDialogProps } from "../dialogs/IDialogProps";
import { getTopic } from "../elements/RoomTopic";
-import { defaultDispatcher } from "../../../dispatcher/dispatcher";
+import { leaveSpace } from "../../../utils/space";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@@ -96,8 +95,6 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
{ error && { error }
}
-
-
{
- defaultDispatcher.dispatch({
- action: "leave_room",
- room_id: space.roomId,
- });
+ leaveSpace(space);
}}
>
{ _t("Leave Space") }
diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
index ec17551d93..b48f5c79c6 100644
--- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
+++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
@@ -18,13 +18,13 @@ import React, { useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
+import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import AliasSettings from "../room_settings/AliasSettings";
import { useStateToggle } from "../../../hooks/useStateToggle";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
-import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
import StyledRadioGroup from "../elements/StyledRadioGroup";
interface IProps {
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index c4a2a8f9d3..bb2184853e 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef, InputHTMLAttributes, LegacyRef } from "react";
+import React, {
+ createRef,
+ MouseEvent,
+ InputHTMLAttributes,
+ LegacyRef,
+ ComponentProps,
+ ComponentType,
+} from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
@@ -23,31 +30,104 @@ import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
-import IconizedContextMenu, {
- IconizedContextMenuOption,
- IconizedContextMenuOptionList,
-} from "../context_menus/IconizedContextMenu";
import { _t } from "../../../languageHandler";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
-import { toRightOf } from "../../structures/ContextMenu";
-import {
- shouldShowSpaceSettings,
- showAddExistingRooms,
- showCreateNewRoom,
- showSpaceInvite,
- showSpaceSettings,
-} from "../../../utils/space";
+import { toRightOf, useContextMenu } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
-import defaultDispatcher from "../../../dispatcher/dispatcher";
-import { Action } from "../../../dispatcher/actions";
-import RoomViewStore from "../../../stores/RoomViewStore";
-import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
-import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
-import { EventType } from "matrix-js-sdk/src/@types/event";
+import AccessibleButton from "../elements/AccessibleButton";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
+import { NotificationState } from "../../../stores/notifications/NotificationState";
+import SpaceContextMenu from "../context_menus/SpaceContextMenu";
+
+interface IButtonProps extends Omit, "title"> {
+ space?: Room;
+ className?: string;
+ selected?: boolean;
+ label: string;
+ contextMenuTooltip?: string;
+ notificationState?: NotificationState;
+ isNarrow?: boolean;
+ avatarSize?: number;
+ ContextMenuComponent?: ComponentType>;
+ onClick(ev: MouseEvent): void;
+}
+
+export const SpaceButton: React.FC = ({
+ space,
+ className,
+ selected,
+ onClick,
+ label,
+ contextMenuTooltip,
+ notificationState,
+ avatarSize,
+ isNarrow,
+ children,
+ ContextMenuComponent,
+ ...props
+}) => {
+ const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
+
+ let avatar = ;
+ if (space) {
+ avatar = ;
+ }
+
+ let notifBadge;
+ if (notificationState) {
+ notifBadge =
+ SpaceStore.instance.setActiveRoomInSpace(space || null)}
+ forceCount={false}
+ notification={notificationState}
+ />
+
;
+ }
+
+ let contextMenu: JSX.Element;
+ if (menuDisplayed && ContextMenuComponent) {
+ contextMenu = ;
+ }
+
+ return (
+
+ { children }
+
+ { avatar }
+ { !isNarrow && { label } }
+ { notifBadge }
+
+ { ContextMenuComponent && }
+
+ { contextMenu }
+
+
+ );
+};
interface IItemProps extends InputHTMLAttributes {
space?: Room;
@@ -61,7 +141,6 @@ interface IItemProps extends InputHTMLAttributes {
interface IItemState {
collapsed: boolean;
- contextMenuPosition: Pick;
childSpaces: Room[];
}
@@ -81,7 +160,6 @@ export class SpaceItem extends React.PureComponent {
this.state = {
collapsed: collapsed,
- contextMenuPosition: null,
childSpaces: this.childSpaces,
};
@@ -124,19 +202,6 @@ export class SpaceItem extends React.PureComponent {
evt.stopPropagation();
};
- private onContextMenu = (ev: React.MouseEvent) => {
- if (this.props.space.getMyMembership() !== "join") return;
- ev.preventDefault();
- ev.stopPropagation();
- this.setState({
- contextMenuPosition: {
- right: ev.clientX,
- top: ev.clientY,
- height: 0,
- },
- });
- };
-
private onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
const action = getKeyBindingsManager().getRoomListAction(ev);
@@ -180,188 +245,6 @@ export class SpaceItem extends React.PureComponent {
SpaceStore.instance.setActiveSpace(this.props.space);
};
- private onMenuOpenClick = (ev: React.MouseEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
- const target = ev.target as HTMLButtonElement;
- this.setState({ contextMenuPosition: target.getBoundingClientRect() });
- };
-
- private onMenuClose = () => {
- this.setState({ contextMenuPosition: null });
- };
-
- private onInviteClick = (ev: ButtonEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- showSpaceInvite(this.props.space);
- this.setState({ contextMenuPosition: null }); // also close the menu
- };
-
- private onSettingsClick = (ev: ButtonEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- showSpaceSettings(this.context, this.props.space);
- this.setState({ contextMenuPosition: null }); // also close the menu
- };
-
- private onLeaveClick = (ev: ButtonEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- defaultDispatcher.dispatch({
- action: "leave_room",
- room_id: this.props.space.roomId,
- });
- this.setState({ contextMenuPosition: null }); // also close the menu
- };
-
- private onNewRoomClick = (ev: ButtonEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- showCreateNewRoom(this.context, this.props.space);
- this.setState({ contextMenuPosition: null }); // also close the menu
- };
-
- private onAddExistingRoomClick = (ev: ButtonEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- showAddExistingRooms(this.context, this.props.space);
- this.setState({ contextMenuPosition: null }); // also close the menu
- };
-
- private onMembersClick = (ev: ButtonEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- if (!RoomViewStore.getRoomId()) {
- defaultDispatcher.dispatch({
- action: "view_room",
- room_id: this.props.space.roomId,
- }, true);
- }
-
- defaultDispatcher.dispatch({
- action: Action.SetRightPanelPhase,
- phase: RightPanelPhases.SpaceMemberList,
- refireParams: { space: this.props.space },
- });
- this.setState({ contextMenuPosition: null }); // also close the menu
- };
-
- private onExploreRoomsClick = (ev: ButtonEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- defaultDispatcher.dispatch({
- action: "view_room",
- room_id: this.props.space.roomId,
- });
- this.setState({ contextMenuPosition: null }); // also close the menu
- };
-
- private renderContextMenu(): React.ReactElement {
- if (this.props.space.getMyMembership() !== "join") return null;
-
- let contextMenu = null;
- if (this.state.contextMenuPosition) {
- const userId = this.context.getUserId();
-
- let inviteOption;
- if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
- inviteOption = (
-
- );
- }
-
- let settingsOption;
- let leaveSection;
- if (shouldShowSpaceSettings(this.context, this.props.space)) {
- settingsOption = (
-
- );
- } else {
- leaveSection =
-
- ;
- }
-
- const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
-
- let newRoomSection;
- if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
- newRoomSection =
-
-
- ;
- }
-
- contextMenu =
-
- { this.props.space.name }
-
-
- { inviteOption }
-
- { settingsOption }
-
-
- { newRoomSection }
- { leaveSection }
- ;
- }
-
- return (
-
-
- { contextMenu }
-
- );
- }
-
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
@@ -369,7 +252,6 @@ export class SpaceItem extends React.PureComponent {
const collapsed = this.isCollapsed;
- const isActive = activeSpaces.includes(space);
const itemClasses = classNames(this.props.className, {
"mx_SpaceItem": true,
"mx_SpaceItem_narrow": isPanelCollapsed,
@@ -378,12 +260,7 @@ export class SpaceItem extends React.PureComponent {
});
const isInvite = space.getMyMembership() === "invite";
- const classes = classNames("mx_SpaceButton", {
- mx_SpaceButton_active: isActive,
- mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
- mx_SpaceButton_narrow: isPanelCollapsed,
- mx_SpaceButton_invite: isInvite,
- });
+
const notificationState = isInvite
? StaticNotificationState.forSymbol("!", NotificationColor.Red)
: SpaceStore.instance.getNotificationState(space.roomId);
@@ -398,19 +275,6 @@ export class SpaceItem extends React.PureComponent {
/>;
}
- let notifBadge;
- if (notificationState) {
- notifBadge =
- SpaceStore.instance.setActiveRoomInSpace(space)}
- forceCount={false}
- notification={notificationState}
- />
-
;
- }
-
- const avatarSize = isNested ? 24 : 32;
-
const toggleCollapseButton = this.state.childSpaces?.length ?
{
return (
-
{ toggleCollapseButton }
-
-
- { !isPanelCollapsed && { space.name } }
- { notifBadge }
- { this.renderContextMenu() }
-
-
+
{ childItems }
diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx
index a2ab760c86..3049d80c72 100644
--- a/src/components/views/voip/AudioFeed.tsx
+++ b/src/components/views/voip/AudioFeed.tsx
@@ -23,9 +23,21 @@ interface IProps {
feed: CallFeed;
}
-export default class AudioFeed extends React.Component {
+interface IState {
+ audioMuted: boolean;
+}
+
+export default class AudioFeed extends React.Component {
private element = createRef();
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ audioMuted: this.props.feed.isAudioMuted(),
+ };
+ }
+
componentDidMount() {
MediaDeviceHandler.instance.addListener(
MediaDeviceHandlerEvent.AudioOutputChanged,
@@ -62,6 +74,7 @@ export default class AudioFeed extends React.Component {
private playMedia() {
const element = this.element.current;
+ if (!element) return;
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
element.muted = false;
element.srcObject = this.props.feed.stream;
@@ -85,6 +98,7 @@ export default class AudioFeed extends React.Component {
private stopMedia() {
const element = this.element.current;
+ if (!element) return;
element.pause();
element.src = null;
@@ -96,10 +110,16 @@ export default class AudioFeed extends React.Component {
}
private onNewStream = () => {
+ this.setState({
+ audioMuted: this.props.feed.isAudioMuted(),
+ });
this.playMedia();
};
render() {
+ // Do not render the audio element if there is no audio track
+ if (this.state.audioMuted) return null;
+
return (
);
diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx
index 895d9773e4..6261b9965f 100644
--- a/src/components/views/voip/CallPreview.tsx
+++ b/src/components/views/voip/CallPreview.tsx
@@ -146,7 +146,7 @@ export default class CallPreview extends React.Component {
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
document.addEventListener("mousemove", this.onMoving);
document.addEventListener("mouseup", this.onEndMoving);
- window.addEventListener("resize", this.snap);
+ window.addEventListener("resize", this.onResize);
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
}
@@ -156,7 +156,7 @@ export default class CallPreview extends React.Component {
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
document.removeEventListener("mousemove", this.onMoving);
document.removeEventListener("mouseup", this.onEndMoving);
- window.removeEventListener("resize", this.snap);
+ window.removeEventListener("resize", this.onResize);
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
@@ -164,6 +164,10 @@ export default class CallPreview extends React.Component {
SettingsStore.unwatchSetting(this.settingsWatcherRef);
}
+ private onResize = (): void => {
+ this.snap(false);
+ };
+
private animationCallback = () => {
// If the PiP isn't being dragged and there is only a tiny difference in
// the desiredTranslation and translation, quit the animationCallback
@@ -207,7 +211,7 @@ export default class CallPreview extends React.Component {
}
}
- private snap = () => {
+ private snap(animate?: boolean): void {
const translationX = this.desiredTranslationX;
const translationY = this.desiredTranslationY;
// We subtract the PiP size from the window size in order to calculate
@@ -236,10 +240,17 @@ export default class CallPreview extends React.Component {
this.desiredTranslationY = PADDING.top;
}
- // We start animating here because we want the PiP to move when we're
- // resizing the window
- this.scheduledUpdate.mark();
- };
+ if (animate) {
+ // We start animating here because we want the PiP to move when we're
+ // resizing the window
+ this.scheduledUpdate.mark();
+ } else {
+ this.setState({
+ translationX: this.desiredTranslationX,
+ translationY: this.desiredTranslationY,
+ });
+ }
+ }
private onRoomViewStoreUpdate = () => {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
@@ -310,7 +321,7 @@ export default class CallPreview extends React.Component {
private onEndMoving = () => {
this.moving = false;
- this.snap();
+ this.snap(true);
};
public render() {
@@ -333,6 +344,7 @@ export default class CallPreview extends React.Component {
secondaryCall={this.state.secondaryCall}
pipMode={true}
onMouseDownOnHeader={this.onStartMoving}
+ onResize={this.onResize}
/>
);
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 8bdd6e0f55..356e642d65 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2021 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -32,6 +33,10 @@ import { avatarUrlForMember } from '../../../Avatar';
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
+import Modal from '../../../Modal';
+import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
+import CallViewSidebar from './CallViewSidebar';
interface IProps {
// The call for us to display
@@ -59,11 +64,15 @@ interface IState {
isRemoteOnHold: boolean;
micMuted: boolean;
vidMuted: boolean;
+ screensharing: boolean;
callState: CallState;
controlsVisible: boolean;
+ hoveringControls: boolean;
showMoreMenu: boolean;
showDialpad: boolean;
- feeds: CallFeed[];
+ primaryFeed: CallFeed;
+ secondaryFeeds: Array;
+ sidebarShown: boolean;
}
function getFullScreenElement() {
@@ -94,7 +103,7 @@ function exitFullscreen() {
if (exitMethod) exitMethod.call(document);
}
-const CONTROLS_HIDE_DELAY = 1000;
+const CONTROLS_HIDE_DELAY = 2000;
// Height of the header duplicated from CSS because we need to subtract it from our max
// height to get the max height of the video
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
@@ -106,20 +115,27 @@ export default class CallView extends React.Component {
private controlsHideTimer: number = null;
private dialpadButton = createRef();
private contextMenuButton = createRef();
+ private contextMenu = createRef();
constructor(props: IProps) {
super(props);
+ const { primary, secondary } = this.getOrderedFeeds(this.props.call.getFeeds());
+
this.state = {
isLocalOnHold: this.props.call.isLocalOnHold(),
isRemoteOnHold: this.props.call.isRemoteOnHold(),
micMuted: this.props.call.isMicrophoneMuted(),
vidMuted: this.props.call.isLocalVideoMuted(),
+ screensharing: this.props.call.isScreensharing(),
callState: this.props.call.state,
controlsVisible: true,
+ hoveringControls: false,
showMoreMenu: false,
showDialpad: false,
- feeds: this.props.call.getFeeds(),
+ primaryFeed: primary,
+ secondaryFeeds: secondary,
+ sidebarShown: true,
};
this.updateCallListeners(null, this.props.call);
@@ -194,7 +210,11 @@ export default class CallView extends React.Component {
};
private onFeedsChanged = (newFeeds: Array) => {
- this.setState({ feeds: newFeeds });
+ const { primary, secondary } = this.getOrderedFeeds(newFeeds);
+ this.setState({
+ primaryFeed: primary,
+ secondaryFeeds: secondary,
+ });
};
private onCallLocalHoldUnhold = () => {
@@ -227,6 +247,7 @@ export default class CallView extends React.Component {
};
private onControlsHideTimer = () => {
+ if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
this.controlsHideTimer = null;
this.setState({
controlsVisible: false,
@@ -237,7 +258,30 @@ export default class CallView extends React.Component {
this.showControls();
};
- private showControls() {
+ private getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } {
+ let primary;
+
+ // Try to use a screensharing as primary, a remote one if possible
+ const screensharingFeeds = feeds.filter((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
+ primary = screensharingFeeds.find((feed) => !feed.isLocal()) || screensharingFeeds[0];
+ // If we didn't find remote screen-sharing stream, try to find any remote stream
+ if (!primary) {
+ primary = feeds.find((feed) => !feed.isLocal());
+ }
+
+ const secondary = [...feeds];
+ // Remove the primary feed from the array
+ if (primary) secondary.splice(secondary.indexOf(primary), 1);
+ secondary.sort((a, b) => {
+ if (a.isLocal() && !b.isLocal()) return -1;
+ if (!a.isLocal() && b.isLocal()) return 1;
+ return 0;
+ });
+
+ return { primary, secondary };
+ }
+
+ private showControls(): void {
if (this.state.showMoreMenu || this.state.showDialpad) return;
if (!this.state.controlsVisible) {
@@ -251,73 +295,62 @@ export default class CallView extends React.Component {
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
}
- private onDialpadClick = () => {
+ private onDialpadClick = (): void => {
if (!this.state.showDialpad) {
- if (this.controlsHideTimer) {
- clearTimeout(this.controlsHideTimer);
- this.controlsHideTimer = null;
- }
-
- this.setState({
- showDialpad: true,
- controlsVisible: true,
- });
+ this.setState({ showDialpad: true });
+ this.showControls();
} else {
- if (this.controlsHideTimer !== null) {
- clearTimeout(this.controlsHideTimer);
- }
- this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
-
- this.setState({
- showDialpad: false,
- });
+ this.setState({ showDialpad: false });
}
};
- private onMicMuteClick = () => {
+ private onMicMuteClick = (): void => {
const newVal = !this.state.micMuted;
this.props.call.setMicrophoneMuted(newVal);
this.setState({ micMuted: newVal });
};
- private onVidMuteClick = () => {
+ private onVidMuteClick = (): void => {
const newVal = !this.state.vidMuted;
this.props.call.setLocalVideoMuted(newVal);
this.setState({ vidMuted: newVal });
};
- private onMoreClick = () => {
- if (this.controlsHideTimer) {
- clearTimeout(this.controlsHideTimer);
- this.controlsHideTimer = null;
- }
+ private onScreenshareClick = async (): Promise => {
+ const isScreensharing = await this.props.call.setScreensharingEnabled(
+ !this.state.screensharing,
+ async (): Promise => {
+ const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
+ const [source] = await finished;
+ return source;
+ },
+ );
this.setState({
- showMoreMenu: true,
- controlsVisible: true,
+ sidebarShown: true,
+ screensharing: isScreensharing,
});
};
- private closeDialpad = () => {
- this.setState({
- showDialpad: false,
- });
- this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
+ private onMoreClick = (): void => {
+ this.setState({ showMoreMenu: true });
+ this.showControls();
};
- private closeContextMenu = () => {
- this.setState({
- showMoreMenu: false,
- });
- this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
+ private closeDialpad = (): void => {
+ this.setState({ showDialpad: false });
+ };
+
+ private closeContextMenu = (): void => {
+ this.setState({ showMoreMenu: false });
};
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
// Note that this assumes we always have a CallView on screen at any given time
// CallHandler would probably be a better place for this
- private onNativeKeyDown = ev => {
+ private onNativeKeyDown = (ev): void => {
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
@@ -347,7 +380,16 @@ export default class CallView extends React.Component {
}
};
- private onRoomAvatarClick = () => {
+ private onCallControlsMouseEnter = (): void => {
+ this.setState({ hoveringControls: true });
+ this.showControls();
+ };
+
+ private onCallControlsMouseLeave = (): void => {
+ this.setState({ hoveringControls: false });
+ };
+
+ private onRoomAvatarClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
dis.dispatch({
action: 'view_room',
@@ -355,7 +397,7 @@ export default class CallView extends React.Component {
});
};
- private onSecondaryRoomAvatarClick = () => {
+ private onSecondaryRoomAvatarClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
dis.dispatch({
@@ -364,50 +406,30 @@ export default class CallView extends React.Component {
});
};
- private onCallResumeClick = () => {
+ private onCallResumeClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
};
- private onTransferClick = () => {
+ private onTransferClick = (): void => {
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
this.props.call.transferToCall(transfereeCall);
};
- public render() {
- const client = MatrixClientPeg.get();
- const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
- const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
- const callRoom = client.getRoom(callRoomId);
- const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
+ private onHangupClick = (): void => {
+ dis.dispatch({
+ action: 'hangup',
+ room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
+ });
+ };
- let dialPad;
- let contextMenu;
-
- if (this.state.showDialpad) {
- dialPad = ;
- }
-
- if (this.state.showMoreMenu) {
- contextMenu = ;
- }
+ private onToggleSidebar = (): void => {
+ this.setState({
+ sidebarShown: !this.state.sidebarShown,
+ });
+ };
+ private renderCallControls(): JSX.Element {
const micClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_micOn: !this.state.micMuted,
@@ -420,6 +442,18 @@ export default class CallView extends React.Component {
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
});
+ const screensharingClasses = classNames({
+ mx_CallView_callControls_button: true,
+ mx_CallView_callControls_button_screensharingOn: this.state.screensharing,
+ mx_CallView_callControls_button_screensharingOff: !this.state.screensharing,
+ });
+
+ const sidebarButtonClasses = classNames({
+ mx_CallView_callControls_button: true,
+ mx_CallView_callControls_button_sidebarOn: this.state.sidebarShown,
+ mx_CallView_callControls_button_sidebarOff: !this.state.sidebarShown,
+ });
+
// Put the other states of the mic/video icons in the document to make sure they're cached
// (otherwise the icon disappears briefly when toggled)
const micCacheClasses = classNames({
@@ -441,59 +475,151 @@ export default class CallView extends React.Component {
mx_CallView_callControls_hidden: !this.state.controlsVisible,
});
- const vidMuteButton = this.props.call.type === CallType.Video ? : null;
+ // We don't support call upgrades (yet) so hide the video mute button in voice calls
+ let vidMuteButton;
+ if (this.props.call.type === CallType.Video) {
+ vidMuteButton = (
+
+ );
+ }
+
+ // Screensharing is possible, if we can send a second stream and
+ // identify it using SDPStreamMetadata or if we can replace the already
+ // existing usermedia track by a screensharing track. We also need to be
+ // connected to know the state of the other side
+ let screensharingButton;
+ if (
+ (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
+ this.props.call.state === CallState.Connected
+ ) {
+ screensharingButton = (
+
+ );
+ }
+
+ // To show the sidebar we need secondary feeds, if we don't have them,
+ // we can hide this button. If we are in PiP, sidebar is also hidden, so
+ // we can hide the button too
+ let sidebarButton;
+ if (
+ !this.props.pipMode &&
+ (
+ this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
+ this.props.call.isScreensharing()
+ )
+ ) {
+ sidebarButton = (
+
+ );
+ }
// The dial pad & 'more' button actions are only relevant in a connected call
- // When not connected, we have to put something there to make the flexbox alignment correct
- const dialpadButton = this.state.callState === CallState.Connected ? :
;
+ let contextMenuButton;
+ if (this.state.callState === CallState.Connected) {
+ contextMenuButton = (
+
+ );
+ }
+ let dialpadButton;
+ if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
+ dialpadButton = (
+
+ );
+ }
- const contextMenuButton = this.state.callState === CallState.Connected ? :
;
+ let dialPad;
+ if (this.state.showDialpad) {
+ dialPad = ;
+ }
- // in the near future, the dial pad button will go on the left. For now, it's the nothing button
- // because something needs to have margin-right: auto to make the alignment correct.
- const callControls =
- { dialpadButton }
-
-
{
- dis.dispatch({
- action: 'hangup',
- room_id: callRoomId,
- });
- }}
- />
- { vidMuteButton }
-
-
- { contextMenuButton }
- ;
+ let contextMenu;
+ if (this.state.showMoreMenu) {
+ contextMenu = ;
+ }
+ return (
+
+ { dialPad }
+ { contextMenu }
+ { dialpadButton }
+
+ { vidMuteButton }
+
+
+ { screensharingButton }
+ { sidebarButton }
+ { contextMenuButton }
+
+
+ );
+ }
+
+ public render() {
+ const client = MatrixClientPeg.get();
+ const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
+ const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
+ const callRoom = client.getRoom(callRoomId);
+ const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
const avatarSize = this.props.pipMode ? 76 : 160;
-
- // The 'content' for the call, ie. the videos for a video call and profile picture
- // for voice calls (fills the bg)
- let contentView: React.ReactNode;
-
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
+ const isScreensharing = this.props.call.isScreensharing();
+ const sidebarShown = this.state.sidebarShown;
+ const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
+ return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
+ });
+ const isVideoCall = this.props.call.type === CallType.Video;
+
+ let contentView: React.ReactNode;
let holdTransferContent;
+
if (transfereeCall) {
const transferTargetRoom = MatrixClientPeg.get().getRoom(
CallHandler.sharedInstance().roomIdForCall(this.props.call),
@@ -539,9 +665,25 @@ export default class CallView extends React.Component {
;
}
+ let sidebar;
+ if (
+ !isOnHold &&
+ !transfereeCall &&
+ sidebarShown &&
+ (isVideoCall || someoneIsScreensharing)
+ ) {
+ sidebar = (
+
+ );
+ }
+
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
if (isOnHold || transfereeCall) {
- if (this.props.call.type === CallType.Video) {
+ if (isVideoCall) {
const containerClasses = classNames({
mx_CallView_content: true,
mx_CallView_video: true,
@@ -560,7 +702,7 @@ export default class CallView extends React.Component {
{ onHoldBackground }
{ holdTransferContent }
- { callControls }
+ { this.renderCallControls() }
);
} else {
@@ -585,7 +727,7 @@ export default class CallView extends React.Component {
{ holdTransferContent }
- { callControls }
+ { this.renderCallControls() }
);
}
@@ -599,81 +741,99 @@ export default class CallView extends React.Component {
mx_CallView_voice: true,
});
- const feeds = this.props.call.getLocalFeeds().map((feed, i) => {
- // Here we check to hide local audio feeds to achieve the same UI/UX
- // as before. But once again this might be subject to change
- if (feed.isVideoMuted()) return;
- return (
-
- );
- });
-
// Saying "Connecting" here isn't really true, but the best thing
// I can come up with, but this might be subject to change as well
- contentView =
- { feeds }
-
-
-
+ contentView = (
+
+ { sidebar }
+
+
{ _t("Connecting") }
+ { this.renderCallControls() }
-
{ _t("Connecting") }
- { callControls }
-
;
+ );
} else {
const containerClasses = classNames({
mx_CallView_content: true,
mx_CallView_video: true,
});
- // TODO: Later the CallView should probably be reworked to support
- // any number of feeds but now we can always expect there to be two
- // feeds. This is because the js-sdk ignores any new incoming streams
- const feeds = this.state.feeds.map((feed, i) => {
- // Here we check to hide local audio feeds to achieve the same UI/UX
- // as before. But once again this might be subject to change
- if (feed.isVideoMuted() && feed.isLocal()) return;
- return (
+ let toast;
+ if (someoneIsScreensharing) {
+ const presentingClasses = classNames({
+ mx_CallView_presenting: true,
+ mx_CallView_presenting_hidden: !this.state.controlsVisible,
+ });
+ const sharerName = this.state.primaryFeed.getMember().name;
+ let text = isScreensharing
+ ? _t("You are presenting")
+ : _t('%(sharerName)s is presenting', { sharerName });
+ if (!this.state.sidebarShown && isVideoCall) {
+ text += " • " + (this.props.call.isLocalVideoMuted()
+ ? _t("Your camera is turned off")
+ : _t("Your camera is still enabled"));
+ }
+
+ toast = (
+
+ { text }
+
+ );
+ }
+
+ contentView = (
+
+ { toast }
+ { sidebar }
- );
- });
-
- contentView =
- { feeds }
- { callControls }
-
;
+ { this.renderCallControls() }
+
+ );
}
- const callTypeText = this.props.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
+ const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
let myClassName;
let fullScreenButton;
- if (this.props.call.type === CallType.Video && !this.props.pipMode) {
- fullScreenButton =
;
+ if (!this.props.pipMode) {
+ fullScreenButton = (
+
+ );
}
let expandButton;
if (this.props.pipMode) {
- expandButton =
;
}
@@ -682,10 +842,15 @@ export default class CallView extends React.Component
{
{ expandButton }
;
+ const callTypeIconClassName = classNames("mx_CallView_header_callTypeIcon", {
+ "mx_CallView_header_callTypeIcon_voice": !isVideoCall,
+ "mx_CallView_header_callTypeIcon_video": isVideoCall,
+ });
+
let header: React.ReactNode;
if (!this.props.pipMode) {
header =
-
+
{ callTypeText }
{ headerControls }
;
@@ -727,8 +892,6 @@ export default class CallView extends React.Component
{
return
{ header }
{ contentView }
- { dialPad }
- { contextMenu }
;
}
}
diff --git a/src/components/views/voip/CallViewSidebar.tsx b/src/components/views/voip/CallViewSidebar.tsx
new file mode 100644
index 0000000000..a0cb25b3df
--- /dev/null
+++ b/src/components/views/voip/CallViewSidebar.tsx
@@ -0,0 +1,53 @@
+/*
+Copyright 2021 Šimon Brandner
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
+import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
+import VideoFeed from "./VideoFeed";
+import classNames from "classnames";
+
+interface IProps {
+ feeds: Array;
+ call: MatrixCall;
+ pipMode: boolean;
+}
+
+export default class CallViewSidebar extends React.Component {
+ render() {
+ const feeds = this.props.feeds.map((feed) => {
+ return (
+
+ );
+ });
+
+ const className = classNames("mx_CallViewSidebar", {
+ mx_CallViewSidebar_pipMode: this.props.pipMode,
+ });
+
+ return (
+
+ { feeds }
+
+ );
+ }
+}
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index 2af8bd6989..3b4a29b3f9 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -68,13 +68,19 @@ export default class Dialpad extends React.PureComponent {
for (let i = 0; i < BUTTONS.length; i++) {
const button = BUTTONS[i];
const digitSubtext = BUTTON_LETTERS[i];
- buttonNodes.push( );
}
if (this.props.hasDial) {
- buttonNodes.push( );
}
diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx
index 0bba65e44f..a36fc37dff 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -81,14 +81,18 @@ export default class DialpadModal extends React.PureComponent {
// Only show the backspace button if the field has content
let dialPadField;
if (this.state.value.length !== 0) {
- dialPadField = ;
} else {
- dialPadField = void;
+
+ primary: boolean;
}
interface IState {
@@ -45,8 +48,8 @@ interface IState {
}
@replaceableComponent("views.voip.VideoFeed")
-export default class VideoFeed extends React.Component {
- private element = createRef();
+export default class VideoFeed extends React.PureComponent {
+ private element: HTMLVideoElement;
constructor(props: IProps) {
super(props);
@@ -58,18 +61,59 @@ export default class VideoFeed extends React.Component {
}
componentDidMount() {
- this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
+ this.updateFeed(null, this.props.feed);
this.playMedia();
}
componentWillUnmount() {
- this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
- this.element.current?.removeEventListener('resize', this.onResize);
- this.stopMedia();
+ this.updateFeed(this.props.feed, null);
+ }
+
+ componentDidUpdate(prevProps: IProps, prevState: IState) {
+ this.updateFeed(prevProps.feed, this.props.feed);
+ // If the mutes state has changed, we try to playMedia()
+ if (
+ prevState.videoMuted !== this.state.videoMuted ||
+ prevProps.feed.stream !== this.props.feed.stream
+ ) {
+ this.playMedia();
+ }
+ }
+
+ static getDerivedStateFromProps(props: IProps) {
+ return {
+ audioMuted: props.feed.isAudioMuted(),
+ videoMuted: props.feed.isVideoMuted(),
+ };
+ }
+
+ private setElementRef = (element: HTMLVideoElement): void => {
+ if (!element) {
+ this.element?.removeEventListener('resize', this.onResize);
+ return;
+ }
+
+ this.element = element;
+ element.addEventListener('resize', this.onResize);
+ };
+
+ private updateFeed(oldFeed: CallFeed, newFeed: CallFeed) {
+ if (oldFeed === newFeed) return;
+
+ if (oldFeed) {
+ this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
+ this.props.feed.removeListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged);
+ this.stopMedia();
+ }
+ if (newFeed) {
+ this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
+ this.props.feed.addListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged);
+ this.playMedia();
+ }
}
private playMedia() {
- const element = this.element.current;
+ const element = this.element;
if (!element) return;
// We play audio in AudioFeed, not here
element.muted = true;
@@ -92,7 +136,7 @@ export default class VideoFeed extends React.Component {
}
private stopMedia() {
- const element = this.element.current;
+ const element = this.element;
if (!element) return;
element.pause();
@@ -109,7 +153,13 @@ export default class VideoFeed extends React.Component {
audioMuted: this.props.feed.isAudioMuted(),
videoMuted: this.props.feed.isVideoMuted(),
});
- this.playMedia();
+ };
+
+ private onMuteStateChanged = () => {
+ this.setState({
+ audioMuted: this.props.feed.isAudioMuted(),
+ videoMuted: this.props.feed.isVideoMuted(),
+ });
};
private onResize = (e) => {
@@ -119,35 +169,58 @@ export default class VideoFeed extends React.Component {
};
render() {
- const videoClasses = {
- mx_VideoFeed: true,
- mx_VideoFeed_local: this.props.feed.isLocal(),
- mx_VideoFeed_remote: !this.props.feed.isLocal(),
+ const { pipMode, primary, feed } = this.props;
+
+ const wrapperClasses = classnames("mx_VideoFeed", {
mx_VideoFeed_voice: this.state.videoMuted,
- mx_VideoFeed_video: !this.state.videoMuted,
- mx_VideoFeed_mirror: (
- this.props.feed.isLocal() &&
- SettingsStore.getValue('VideoView.flipVideoHorizontally')
- ),
- };
+ });
+ const micIconClasses = classnames("mx_VideoFeed_mic", {
+ mx_VideoFeed_mic_muted: this.state.audioMuted,
+ mx_VideoFeed_mic_unmuted: !this.state.audioMuted,
+ });
- if (this.state.videoMuted) {
- const member = this.props.feed.getMember();
- const avatarSize = this.props.pipMode ? 76 : 160;
-
- return (
-
-
-
- );
- } else {
- return (
-
+ let micIcon;
+ if (feed.purpose !== SDPStreamMetadataPurpose.Screenshare && !pipMode) {
+ micIcon = (
+
);
}
+
+ let content;
+ if (this.state.videoMuted) {
+ const member = this.props.feed.getMember();
+
+ let avatarSize;
+ if (pipMode && primary) avatarSize = 76;
+ else if (pipMode && !primary) avatarSize = 16;
+ else if (!pipMode && primary) avatarSize = 160;
+ else; // TBD
+
+ content =(
+
+ );
+ } else {
+ const videoClasses = classnames("mx_VideoFeed_video", {
+ mx_VideoFeed_video_mirror: (
+ this.props.feed.isLocal() &&
+ SettingsStore.getValue('VideoView.flipVideoHorizontally')
+ ),
+ });
+
+ content= (
+
+ );
+ }
+
+ return (
+
+ { micIcon }
+ { content }
+
+ );
}
}
diff --git a/src/createRoom.ts b/src/createRoom.ts
index effc6ec1ac..31774bf56f 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -18,7 +18,15 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
-import { EventType } from "matrix-js-sdk/src/@types/event";
+import { EventType, RoomCreateTypeField, RoomType } from "matrix-js-sdk/src/@types/event";
+import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
+import {
+ HistoryVisibility,
+ JoinRule,
+ Preset,
+ RestrictedAllowType,
+ Visibility,
+} from "matrix-js-sdk/src/@types/partials";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
@@ -35,8 +43,6 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space";
import { Action } from "./dispatcher/actions";
-import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
-import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import Spinner from "./components/views/elements/Spinner";
@@ -52,7 +58,11 @@ export interface IOpts {
inlineErrors?: boolean;
andView?: boolean;
associatedWithCommunity?: string;
+ avatar?: File | string; // will upload if given file, else mxcUrl is needed
+ roomType?: RoomType | string;
+ historyVisibility?: HistoryVisibility;
parentSpace?: Room;
+ joinRule?: JoinRule;
}
/**
@@ -74,7 +84,7 @@ export interface IOpts {
* @returns {Promise} which resolves to the room id, or null if the
* action was aborted or failed.
*/
-export default function createRoom(opts: IOpts): Promise {
+export default async function createRoom(opts: IOpts): Promise {
opts = opts || {};
if (opts.spinner === undefined) opts.spinner = true;
if (opts.guestAccess === undefined) opts.guestAccess = true;
@@ -85,7 +95,7 @@ export default function createRoom(opts: IOpts): Promise {
const client = MatrixClientPeg.get();
if (client.isGuest()) {
dis.dispatch({ action: 'require_registration' });
- return Promise.resolve(null);
+ return null;
}
const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
@@ -111,6 +121,13 @@ export default function createRoom(opts: IOpts): Promise {
createOpts.is_direct = true;
}
+ if (opts.roomType) {
+ createOpts.creation_content = {
+ ...createOpts.creation_content,
+ [RoomCreateTypeField]: opts.roomType,
+ };
+ }
+
// By default, view the room after creating it
if (opts.andView === undefined) {
opts.andView = true;
@@ -142,11 +159,56 @@ export default function createRoom(opts: IOpts): Promise {
}
if (opts.parentSpace) {
- opts.createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
- opts.createOpts.initial_state.push({
+ createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
+ if (!opts.historyVisibility) {
+ opts.historyVisibility = createOpts.preset === Preset.PublicChat
+ ? HistoryVisibility.WorldReadable
+ : HistoryVisibility.Invited;
+ }
+
+ if (opts.joinRule === JoinRule.Restricted) {
+ if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) {
+ createOpts.room_version = SpaceStore.instance.restrictedJoinRuleSupport.preferred;
+
+ createOpts.initial_state.push({
+ type: EventType.RoomJoinRules,
+ content: {
+ "join_rule": JoinRule.Restricted,
+ "allow": [{
+ "type": RestrictedAllowType.RoomMembership,
+ "room_id": opts.parentSpace.roomId,
+ }],
+ },
+ });
+ }
+ }
+ }
+
+ // we handle the restricted join rule in the parentSpace handling block above
+ if (opts.joinRule && opts.joinRule !== JoinRule.Restricted) {
+ createOpts.initial_state.push({
+ type: EventType.RoomJoinRules,
+ content: { join_rule: opts.joinRule },
+ });
+ }
+
+ if (opts.avatar) {
+ let url = opts.avatar;
+ if (opts.avatar instanceof File) {
+ url = await client.uploadContent(opts.avatar);
+ }
+
+ createOpts.initial_state.push({
+ type: EventType.RoomAvatar,
+ content: { url },
+ });
+ }
+
+ if (opts.historyVisibility) {
+ createOpts.initial_state.push({
type: EventType.RoomHistoryVisibility,
content: {
- "history_visibility": opts.createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
+ "history_visibility": opts.historyVisibility,
},
});
}
diff --git a/src/customisations/WidgetVariables.ts b/src/customisations/WidgetVariables.ts
new file mode 100644
index 0000000000..db3a56436d
--- /dev/null
+++ b/src/customisations/WidgetVariables.ts
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+// Populate this class with the details of your customisations when copying it.
+import { ITemplateParams } from "matrix-widget-api";
+
+/**
+ * Provides a partial set of the variables needed to render any widget. If
+ * variables are missing or not provided then they will be filled with the
+ * application-determined defaults.
+ *
+ * This will not be called until after isReady() resolves.
+ * @returns {Partial>} The variables.
+ */
+function provideVariables(): Partial> {
+ return {};
+}
+
+/**
+ * Resolves to whether or not the customisation point is ready for variables
+ * to be provided. This will block widgets being rendered.
+ * @returns {Promise} Resolves when ready.
+ */
+async function isReady(): Promise {
+ return; // default no waiting
+}
+
+// This interface summarises all available customisation points and also marks
+// them all as optional. This allows customisers to only define and export the
+// customisations they need while still maintaining type safety.
+export interface IWidgetVariablesCustomisations {
+ provideVariables?: typeof provideVariables;
+
+ // If not provided, the app will assume that the customisation is always ready.
+ isReady?: typeof isReady;
+}
+
+// A real customisation module will define and export one or more of the
+// customisation points that make up the interface above.
+export const WidgetVariableCustomisations: IWidgetVariablesCustomisations = {};
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index 5732428201..2a8ce7a08b 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -193,4 +193,16 @@ export enum Action {
* Switches space. Should be used with SwitchSpacePayload.
*/
SwitchSpace = "switch_space",
+
+ /**
+ * Signals to the visible space hierarchy that a change has occurred an that it should refresh.
+ */
+ UpdateSpaceHierarchy = "update_space_hierarchy",
+
+ /**
+ * Fires when a monitored setting is updated,
+ * see SettingsStore::monitorSetting for more details.
+ * Should be used with SettingUpdatedPayload.
+ */
+ SettingUpdated = "setting_updated",
}
diff --git a/src/dispatcher/payloads/SettingUpdatedPayload.ts b/src/dispatcher/payloads/SettingUpdatedPayload.ts
new file mode 100644
index 0000000000..8d457facfb
--- /dev/null
+++ b/src/dispatcher/payloads/SettingUpdatedPayload.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { ActionPayload } from "../payloads";
+import { Action } from "../actions";
+import { SettingLevel } from "../../settings/SettingLevel";
+
+export interface SettingUpdatedPayload extends ActionPayload {
+ action: Action.SettingUpdated;
+
+ settingName: string;
+ roomId: string;
+ level: SettingLevel;
+ newValueAtLevel: SettingLevel;
+ newValue: any;
+}
diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index eb8adfda9d..9033f99b6c 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -121,6 +121,12 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
return partCreator.plain(`\`${n.textContent}\``);
case "DEL":
return partCreator.plain(`${n.textContent}`);
+ case "SUB":
+ return partCreator.plain(`${n.textContent} `);
+ case "SUP":
+ return partCreator.plain(`${n.textContent} `);
+ case "U":
+ return partCreator.plain(`${n.textContent} `);
case "LI": {
const indent = " ".repeat(state.listDepth - 1);
if (n.parentElement.nodeName === "OL") {
diff --git a/src/hooks/useEventEmitter.ts b/src/hooks/useEventEmitter.ts
index a81bba5699..74b23f0198 100644
--- a/src/hooks/useEventEmitter.ts
+++ b/src/hooks/useEventEmitter.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { useRef, useEffect } from "react";
+import { useRef, useEffect, useState, useCallback } from "react";
import type { EventEmitter } from "events";
type Handler = (...args: any[]) => void;
@@ -48,3 +48,14 @@ export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbo
[eventName, emitter], // Re-run if eventName or emitter changes
);
};
+
+type Mapper = (...args: any[]) => T;
+
+export const useEventEmitterState = (emitter: EventEmitter, eventName: string | symbol, fn: Mapper): T => {
+ const [value, setValue] = useState(fn());
+ const handler = useCallback((...args: any[]) => {
+ setValue(fn(...args));
+ }, [fn]);
+ useEventEmitter(emitter, eventName, handler);
+ return value;
+};
diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json
index cc63995e0f..a14e6f8ce8 100644
--- a/src/i18n/strings/ar.json
+++ b/src/i18n/strings/ar.json
@@ -1552,5 +1552,15 @@
"Too Many Calls": "مكالمات كثيرة جدا",
"Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا , من فضلك قم بالتأكد.",
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.",
- "Explore rooms": "استكشِف الغرف"
+ "Explore rooms": "استكشِف الغرف",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "قد يؤدي استخدام عنصر واجهة المستخدم هذا إلى مشاركة البيانات مع %(widgetDomain)s ومدير التكامل الخاص بك.",
+ "Identity server is": "خادم الهوية هو",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط ، ويمكنهم تعديل عناصر واجهة المستخدم ، وإرسال دعوات الغرف ، وتعيين مستويات القوة نيابة عنك.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل (%(serverName)s) لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
+ "Identity server": "خادم الهوية",
+ "Identity server (%(server)s)": "خادمة الهوية (%(server)s)",
+ "Could not connect to identity server": "تعذر الاتصال بخادم هوية",
+ "Not a valid identity server (status code %(code)s)": "خادم هوية مردود (رقم الحال %(code)s)",
+ "Identity server URL must be HTTPS": "يجب أن يكون رابط (URL) خادم الهوية HTTPS"
}
diff --git a/src/i18n/strings/az.json b/src/i18n/strings/az.json
index 987cef73b2..b460df0bf8 100644
--- a/src/i18n/strings/az.json
+++ b/src/i18n/strings/az.json
@@ -383,5 +383,7 @@
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu.",
"Create Account": "Hesab Aç",
"Explore rooms": "Otaqları kəşf edin",
- "Sign In": "Daxil ol"
+ "Sign In": "Daxil ol",
+ "Identity server is": "Eyniləşdirmənin serveri bu",
+ "Identity server": "Eyniləşdirmənin serveri"
}
diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json
index 294d5a4979..19d95842c8 100644
--- a/src/i18n/strings/bg.json
+++ b/src/i18n/strings/bg.json
@@ -2897,5 +2897,17 @@
"Already in call": "Вече в разговор",
"You're already in a call with this person.": "Вече сте в разговор в този човек.",
"Too Many Calls": "Твърде много повиквания",
- "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно."
+ "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно.",
+ "Integration manager": "Мениджър на интеграции",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Вашият %(brand)s не позволява да използвате мениджъра на интеграции за да направите това. Свържете се с администратор.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Използването на това приспособление може да сподели данни с %(widgetDomain)s и с мениджъра на интеграции.",
+ "Identity server is": "Сървър за самоличност:",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Мениджърът на интеграции получава конфигурационни данни, може да модифицира приспособления, да изпраща покани за стаи и да настройва нива на достъп от ваше име.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции за управление на ботове, приспособления и стикери.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции %(serverName)s за управление на ботове, приспособления и стикери.",
+ "Identity server": "Сървър за самоличност",
+ "Identity server (%(server)s)": "Сървър за самоличност (%(server)s)",
+ "Could not connect to identity server": "Неуспешна връзка със сървъра за самоличност",
+ "Not a valid identity server (status code %(code)s)": "Невалиден сървър за самоличност (статус код %(code)s)",
+ "Identity server URL must be HTTPS": "Адресът на сървъра за самоличност трябва да бъде HTTPS"
}
diff --git a/src/i18n/strings/bn_BD.json b/src/i18n/strings/bn_BD.json
index 9e26dfeeb6..5ceda07ab4 100644
--- a/src/i18n/strings/bn_BD.json
+++ b/src/i18n/strings/bn_BD.json
@@ -1 +1,4 @@
-{}
\ No newline at end of file
+{
+ "Integration manager": "ইন্টিগ্রেশন ম্যানেজার",
+ "Identity server": "পরিচয় সার্ভার"
+}
diff --git a/src/i18n/strings/bn_IN.json b/src/i18n/strings/bn_IN.json
index 0967ef424b..5ceda07ab4 100644
--- a/src/i18n/strings/bn_IN.json
+++ b/src/i18n/strings/bn_IN.json
@@ -1 +1,4 @@
-{}
+{
+ "Integration manager": "ইন্টিগ্রেশন ম্যানেজার",
+ "Identity server": "পরিচয় সার্ভার"
+}
diff --git a/src/i18n/strings/bs.json b/src/i18n/strings/bs.json
index dc4ebda993..a7891ebdcd 100644
--- a/src/i18n/strings/bs.json
+++ b/src/i18n/strings/bs.json
@@ -2,5 +2,6 @@
"Dismiss": "Odbaci",
"Create Account": "Otvori račun",
"Sign In": "Prijavite se",
- "Explore rooms": "Istražite sobe"
+ "Explore rooms": "Istražite sobe",
+ "Identity server": "Identifikacioni Server"
}
diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json
index 945b5a10cc..01d082b6a2 100644
--- a/src/i18n/strings/ca.json
+++ b/src/i18n/strings/ca.json
@@ -953,5 +953,10 @@
"Unable to access microphone": "No s'ha pogut accedir al micròfon",
"Explore rooms": "Explora sales",
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sno ha fet canvis",
- "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops"
+ "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops",
+ "Integration manager": "Gestor d'integracions",
+ "Identity server is": "El servidor d'identitat és",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Els gestors d'integracions reben dades de configuració i poden modificar ginys, enviar invitacions a sales i establir nivells d'autoritat en nom teu.",
+ "Identity server": "Servidor d'identitat",
+ "Could not connect to identity server": "No s'ha pogut connectar amb el servidor d'identitat"
}
diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 266fa339d2..940ab1a79a 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -857,7 +857,7 @@
"Failed to upgrade room": "Nepovedlo se upgradeovat místnost",
"The room upgrade could not be completed": "Upgrade místnosti se nepovedlo dokončit",
"Upgrade this room to version %(version)s": "Upgradování místnosti na verzi %(version)s",
- "Security & Privacy": "Zabezpečení",
+ "Security & Privacy": "Zabezpečení a soukromí",
"Encryption": "Šifrování",
"Once enabled, encryption cannot be disabled.": "Po zapnutí, už nepůjde šifrování vypnout.",
"Encrypted": "Šifrováno",
@@ -1061,7 +1061,7 @@
"Anchor": "Kotva",
"Headphones": "Sluchátka",
"Folder": "Desky",
- "Pin": "Připínáček",
+ "Pin": "Připnout",
"Yes": "Ano",
"No": "Ne",
"Never lose encrypted messages": "Nikdy nepřijdete o šifrované zprávy",
@@ -2250,7 +2250,7 @@
"Send feedback": "Odeslat zpětnou vazbu",
"Feedback": "Zpětná vazba",
"Feedback sent": "Zpětná vazba byla odeslána",
- "Security & privacy": "Zabezpečení",
+ "Security & privacy": "Zabezpečení a soukromí",
"All settings": "Všechna nastavení",
"Start a conversation with someone using their name, email address or username (like ).": "Napište jméno nebo emailovou adresu uživatele se kterým chcete začít konverzaci (např. ).",
"Start a new chat": "Založit novou konverzaci",
@@ -3322,7 +3322,7 @@
"You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Kliknutím na avatar na panelu filtrů můžete kdykoli zobrazit pouze místnosti a lidi spojené s danou komunitou.",
"Move down": "Posun dolů",
"Move up": "Posun nahoru",
- "Report": "Zpráva",
+ "Report": "Nahlásit",
"Collapse reply thread": "Sbalit vlákno odpovědi",
"Show preview": "Zobrazit náhled",
"View source": "Zobrazit zdroj",
@@ -3400,5 +3400,107 @@
"Some invites couldn't be sent": "Některé pozvánky nebylo možné odeslat",
"We sent the others, but the below people couldn't be invited to ": "Poslali jsme ostatním, ale níže uvedení lidé nemohli být pozváni do ",
"Visibility": "Viditelnost",
- "Address": "Adresa"
+ "Address": "Adresa",
+ "To view all keyboard shortcuts, click here.": "Pro zobrazení všech klávesových zkratek, klikněte zde.",
+ "Unnamed audio": "Nepojmenovaný audio soubor",
+ "Error processing audio message": "Došlo k chybě při zpracovávání hlasové zprávy",
+ "Images, GIFs and videos": "Obrázky, GIFy a videa",
+ "Code blocks": "Bloky kódu",
+ "Displaying time": "Zobrazování času",
+ "Keyboard shortcuts": "Klávesové zkratky",
+ "Use Ctrl + F to search timeline": "Stiskněte Ctrl + F k vyhledávání v časové ose",
+ "Use Command + F to search timeline": "Stiskněte Command + F k vyhledávání v časové ose",
+ "Integration manager": "Správce integrací",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Váš %(brand)s neumožňuje použít správce integrací. Kontaktujte prosím správce.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Použití tohoto widgetu může sdílet data s %(widgetDomain)s a vaším správcem integrací.",
+ "Identity server is": "Server identity je",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Správci integrace přijímají konfigurační data a mohou vaším jménem upravovat widgety, odesílat pozvánky do místností a nastavovat úrovně oprávnění.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Použít správce integrací na správu botů, widgetů a samolepek.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Použít správce integrací (%(serverName)s) na správu botů, widgetů a samolepek.",
+ "Identity server": "Server identit",
+ "Identity server (%(server)s)": "Server identit (%(server)s)",
+ "Could not connect to identity server": "Nepodařilo se připojit k serveru identit",
+ "Not a valid identity server (status code %(code)s)": "Toto není platný server identit (stavový kód %(code)s)",
+ "Identity server URL must be HTTPS": "Adresa serveru identit musí být na HTTPS",
+ "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.": "Upozorňujeme, že aktualizací vznikne nová verze místnosti . Všechny aktuální zprávy zůstanou v této archivované místnosti.",
+ "Automatically invite members from this room to the new one": "Automaticky pozve členy této místnosti do nové místnosti",
+ "These are likely ones other room admins are a part of.": "Pravděpodobně se jedná o ty, kterých se účastní i ostatní správci místností.",
+ "Other spaces or rooms you might not know": "Další prostory nebo místnosti, které možná neznáte",
+ "Spaces you know that contain this room": "Prostory, které znáte a které obsahují tuto místnost",
+ "Search spaces": "Hledat prostory",
+ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Rozhodněte, které prostory mají přístup do této místnosti. Pokud je vybrán prostor, mohou jeho členové najít a připojit se k němu.",
+ "Select spaces": "Vybrané prostory",
+ "You're removing all spaces. Access will default to invite only": "Odstraňujete všechny prostory. Přístup bude ve výchozím nastavení pouze na pozvánky",
+ "User Directory": "Adresář uživatelů",
+ "Connected": "Připojeno",
+ "& %(count)s more|other": "a %(count)s dalších",
+ "Only invited people can join.": "Připojit se mohou pouze pozvané osoby.",
+ "Private (invite only)": "Soukromé (pouze pro pozvané)",
+ "This upgrade will allow members of selected spaces access to this room without an invite.": "Tato změna umožní členům vybraných prostorů přístup do této místnosti bez pozvánky.",
+ "There was an error loading your notification settings.": "Došlo k chybě při načítání nastavení oznámení.",
+ "Global": "Globální",
+ "Enable email notifications for %(email)s": "Povolení e-mailových oznámení pro %(email)s",
+ "Enable for this account": "Povolit pro tento účet",
+ "An error occurred whilst saving your notification preferences.": "Při ukládání předvoleb oznámení došlo k chybě.",
+ "Error saving notification preferences": "Chyba při ukládání předvoleb oznámení",
+ "Messages containing keywords": "Zprávy obsahující klíčová slova",
+ "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Díky tomuto mohou místnosti zůstat soukromé a zároveň je mohou lidé v prostoru najít a připojit se k nim. Všechny nové místnosti v prostoru budou mít tuto možnost k dispozici.",
+ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Chcete-li členům prostoru pomoci najít soukromou místnost a připojit se k ní, přejděte do nastavení Zabezpečení a soukromí dané místnosti.",
+ "Error downloading audio": "Chyba při stahování audia",
+ "Unknown failure: %(reason)s)": "Neznámá chyba: %(reason)s",
+ "No answer": "Žádná odpověď",
+ "An unknown error occurred": "Došlo k neznámé chybě",
+ "Their device couldn't start the camera or microphone": "Jejich zařízení nemohlo spustit kameru nebo mikrofon",
+ "Connection failed": "Spojení se nezdařilo",
+ "Could not connect media": "Nepodařilo se připojit média",
+ "This call has ended": "Tento hovor byl ukončen",
+ "Unable to copy a link to the room to the clipboard.": "Nelze zkopírovat odkaz na místnost do schránky.",
+ "Unable to copy room link": "Nelze zkopírovat odkaz na místnost",
+ "This call has failed": "Toto volání se nezdařilo",
+ "Anyone can find and join.": "Kdokoliv může najít a připojit se.",
+ "Room visibility": "Viditelnost místnosti",
+ "Visible to space members": "Viditelné pro členy prostoru",
+ "Public room": "Veřejná místnost",
+ "Private room (invite only)": "Soukromá místnost (pouze pro pozvané)",
+ "Create a room": "Vytvořit místnost",
+ "Only people invited will be able to find and join this room.": "Tuto místnost budou moci najít a připojit se k ní pouze pozvaní lidé.",
+ "Anyone will be able to find and join this room, not just members of .": "Tuto místnost bude moci najít a připojit se k ní kdokoli, nejen členové .",
+ "You can change this at any time from room settings.": "Tuto hodnotu můžete kdykoli změnit v nastavení místnosti.",
+ "Everyone in will be able to find and join this room.": "Všichni v budou moci tuto místnost najít a připojit se k ní.",
+ "Image": "Obrázek",
+ "Sticker": "Nálepka",
+ "Downloading": "Stahování",
+ "The call is in an unknown state!": "Hovor je v neznámém stavu!",
+ "Call back": "Zavolat zpět",
+ "You missed this call": "Zmeškali jste tento hovor",
+ "The voice message failed to upload.": "Hlasovou zprávu se nepodařilo nahrát.",
+ "Copy Room Link": "Kopírovat odkaz na místnost",
+ "Show %(count)s other previews|one": "Zobrazit %(count)s další náhled",
+ "Show %(count)s other previews|other": "Zobrazit %(count)s dalších náhledů",
+ "Access": "Přístup",
+ "People with supported clients will be able to join the room without having a registered account.": "Lidé s podporovanými klienty se budou moci do místnosti připojit, aniž by měli registrovaný účet.",
+ "Decide who can join %(roomName)s.": "Rozhodněte, kdo se může připojit k %(roomName)s.",
+ "Space members": "Členové prostoru",
+ "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Každý, kdo se nachází v prostoru %(spaceName)s, ho může najít a připojit se k němu. Můžete vybrat i jiné prostory.",
+ "Anyone in a space can find and join. You can select multiple spaces.": "Každý, kdo se nachází v prostoru, ho může najít a připojit se k němu. Můžete vybrat více prostorů.",
+ "Spaces with access": "Prostory s přístupem",
+ "Anyone in a space can find and join. Edit which spaces can access here. ": "Každý, kdo se nachází v prostoru, ho může najít a připojit se k němu. Zde upravte, ke kterým prostorům lze přistupovat. ",
+ "Currently, %(count)s spaces have access|other": "V současné době má %(count)s prostorů přístup k",
+ "Upgrade required": "Vyžadována aktualizace",
+ "Mentions & keywords": "Zmínky a klíčová slova",
+ "Message bubbles": "Bubliny zpráv",
+ "IRC": "IRC",
+ "New keyword": "Nové klíčové slovo",
+ "Keyword": "Klíčové slovo",
+ "New layout switcher (with message bubbles)": "Nový přepínač rozložení (s bublinami zpráv)",
+ "Help space members find private rooms": "Pomoci členům prostorů najít soukromé místnosti",
+ "Help people in spaces to find and join private rooms": "Pomoci lidem v prostorech najít soukromé místnosti a připojit se k nim",
+ "New in the Spaces beta": "Nové v betaverzi Spaces",
+ "User %(userId)s is already invited to the room": "Uživatel %(userId)s je již pozván do místnosti",
+ "Transfer Failed": "Přepojení se nezdařilo",
+ "Unable to transfer call": "Nelze přepojit hovor",
+ "They didn't pick up": "Nezvedli to",
+ "Call again": "Volat znova",
+ "They declined this call": "Odmítli tento hovor",
+ "You declined this call": "Odmítli jste tento hovor"
}
diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json
index b99b834636..2b4af70877 100644
--- a/src/i18n/strings/cy.json
+++ b/src/i18n/strings/cy.json
@@ -11,5 +11,6 @@
"Sign In": "Mewngofnodi",
"Create Account": "Creu Cyfrif",
"Dismiss": "Wfftio",
- "Explore rooms": "Archwilio Ystafelloedd"
+ "Explore rooms": "Archwilio Ystafelloedd",
+ "Identity server": "Gweinydd Adnabod"
}
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 1def5b300e..4a2b6521b2 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -15,7 +15,7 @@
"Bans user with given id": "Verbannt den Benutzer mit der angegebenen ID",
"Deops user with given id": "Setzt das Berechtigungslevel beim Benutzer mit der angegebenen ID zurück",
"Invites user with given id to current room": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein",
- "Kicks user with given id": "Benutzer mit der angegebenen ID kicken",
+ "Kicks user with given id": "Benutzer mit der angegebenen ID entfernen",
"Changes your display nickname": "Ändert deinen Anzeigenamen",
"Change Password": "Passwort ändern",
"Searches DuckDuckGo for results": "Verwendet DuckDuckGo zum Suchen",
@@ -204,14 +204,14 @@
"Failed to ban user": "Verbannen des Benutzers fehlgeschlagen",
"Failed to change power level": "Ändern der Berechtigungsstufe fehlgeschlagen",
"Failed to join room": "Betreten des Raumes ist fehlgeschlagen",
- "Failed to kick": "Rauswurf fehlgeschlagen",
+ "Failed to kick": "Entfernen fehlgeschlagen",
"Failed to mute user": "Stummschalten des Nutzers fehlgeschlagen",
"Failed to reject invite": "Ablehnen der Einladung ist fehlgeschlagen",
"Failed to set display name": "Anzeigename konnte nicht geändert werden",
"Fill screen": "Fülle Bildschirm",
"Incorrect verification code": "Falscher Verifizierungscode",
"Join Room": "Raum beitreten",
- "Kick": "Rausschmeißen",
+ "Kick": "Entfernen",
"not specified": "nicht angegeben",
"No more results": "Keine weiteren Ergebnisse",
"No results": "Keine Ergebnisse",
@@ -539,10 +539,10 @@
"were banned %(count)s times|one": "wurden verbannt",
"was banned %(count)s times|other": "wurde %(count)s-mal verbannt",
"was banned %(count)s times|one": "wurde verbannt",
- "were kicked %(count)s times|other": "wurden %(count)s-mal rausgeworfen",
- "were kicked %(count)s times|one": "wurden rausgeworfen",
- "was kicked %(count)s times|other": "wurde %(count)s-mal rausgeworfen",
- "was kicked %(count)s times|one": "wurde rausgeworfen",
+ "were kicked %(count)s times|other": "wurden %(count)s-mal entfernt",
+ "were kicked %(count)s times|one": "wurden entfernt",
+ "was kicked %(count)s times|other": "wurde %(count)s-mal entfernt",
+ "was kicked %(count)s times|one": "wurde entfernt",
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)shaben %(count)s-mal ihren Namen geändert",
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)shaben ihren Namen geändert",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)shat %(count)s-mal den Namen geändert",
@@ -551,7 +551,7 @@
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)shat das Profilbild %(count)s-mal geändert",
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)shat das Profilbild geändert",
"Disinvite this user?": "Einladung für diesen Benutzer zurückziehen?",
- "Kick this user?": "Diesen Benutzer rausschmeißen?",
+ "Kick this user?": "Diesen Benutzer entfernen?",
"Unban this user?": "Verbannung für diesen Benutzer aufheben?",
"Ban this user?": "Diesen Benutzer verbannen?",
"Members only (since the point in time of selecting this option)": "Mitglieder",
@@ -979,7 +979,7 @@
"Render simple counters in room header": "Einfache Zähler in Raumkopfzeile anzeigen",
"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 join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Einladungen/Entfernen/Banne)",
"Show avatar changes": "Avataränderungen",
"Show display name changes": "Änderungen von Anzeigenamen",
"Send typing notifications": "Tippbenachrichtigungen senden",
@@ -1211,7 +1211,7 @@
"Send messages": "Nachrichten senden",
"Invite users": "Benutzer einladen",
"Change settings": "Einstellungen ändern",
- "Kick users": "Benutzer kicken",
+ "Kick users": "Benutzer entfernen",
"Ban users": "Benutzer verbannen",
"Remove messages": "Nachrichten löschen",
"Notify everyone": "Jeden benachrichtigen",
@@ -1644,7 +1644,7 @@
"Failed to set topic": "Das Festlegen des Themas ist fehlgeschlagen",
"Command failed": "Befehl fehlgeschlagen",
"Could not find user in room": "Benutzer konnte nicht im Raum gefunden werden",
- "Click the button below to confirm adding this email address.": "Klicke unten auf die Schaltfläche, um die hinzugefügte E-Mail-Adresse zu bestätigen.",
+ "Click the button below to confirm adding this email address.": "Klicke unten auf den Knopf, um die hinzugefügte E-Mail-Adresse zu bestätigen.",
"Confirm adding phone number": "Hinzugefügte Telefonnummer bestätigen",
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschlussregel für Server von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s erneuert eine Ausschlussregel von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
@@ -3371,8 +3371,8 @@
"See when people join, leave, or are invited to your active room": "Anzeigen, wenn Leute den aktuellen Raum betreten, verlassen oder in ihn eingeladen werden",
"Teammates might not be able to view or join any private rooms you make.": "Mitglieder werden private Räume möglicherweise weder sehen noch betreten können.",
"Error - Mixed content": "Fehler - Uneinheitlicher Inhalt",
- "Kick, ban, or invite people to your active room, and make you leave": "Den aktiven Raum verlassen, Leute einladen, kicken oder bannen",
- "Kick, ban, or invite people to this room, and make you leave": "Diesen Raum verlassen, Leute einladen, kicken oder bannen",
+ "Kick, ban, or invite people to your active room, and make you leave": "Den aktiven Raum verlassen, Leute einladen, entfernen oder bannen",
+ "Kick, ban, or invite people to this room, and make you leave": "Diesen Raum verlassen, Leute einladen, entfernen oder bannen",
"View source": "Rohdaten anzeigen",
"What this user is writing is wrong.\nThis will be reported to the room moderators.": "Die Person verbreitet Falschinformation.\nDies wird an die Raummoderation gemeldet.",
"[number]": "[Nummer]",
@@ -3427,8 +3427,8 @@
"Show all rooms in Home": "Alle Räume auf der Startseite zeigen",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Inhalte an Mods melden. In Räumen, die Moderation unterstützen, kannst du so unerwünschte Inhalte direkt der Raummoderation melden",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s hat die angehefteten Nachrichten geändert.",
- "%(senderName)s kicked %(targetName)s": "%(senderName)s hat %(targetName)s gekickt",
- "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s hat %(targetName)s gekickt: %(reason)s",
+ "%(senderName)s kicked %(targetName)s": "%(senderName)s hat %(targetName)s entfernt",
+ "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s hat %(targetName)s entfernt: %(reason)s",
"%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen",
"%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen: %(reason)s",
"%(senderName)s unbanned %(targetName)s": "%(senderName)s hat %(targetName)s entbannt",
@@ -3442,7 +3442,7 @@
"%(senderName)s removed their profile picture": "%(senderName)s hat das Profilbild entfernt",
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s hat den alten Nicknamen %(oldDisplayName)s entfernt",
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s hat den Nicknamen zu %(displayName)s geändert",
- "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s hat den Nicknamen zu%(displayName)s geändert",
+ "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s hat den Nicknamen zu %(displayName)s geändert",
"%(senderName)s banned %(targetName)s": "%(senderName)s hat %(targetName)s gebannt",
"%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s hat %(targetName)s gebannt: %(reason)s",
"%(targetName)s accepted an invitation": "%(targetName)s hat die Einladung akzeptiert",
@@ -3452,5 +3452,63 @@
"Message search initialisation failed, check your settings for more information": "Initialisierung der Nachrichtensuche fehlgeschlagen. Öffne die Einstellungen für mehr Information.",
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "Der Raum beinhaltet illegale oder toxische Nachrichten und die Raummoderation verhindert es nicht.\nDies wird an die Betreiber von %(homeserver)s gemeldet werden.",
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "Der Raum beinhaltet illegale oder toxische Nachrichten und die Raummoderation verhindert es nicht.\nDies wird an die Betreiber von %(homeserver)s gemeldet werden. Diese können jedoch die verschlüsselten Nachrichten nicht lesen.",
- "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Diese Person zeigt illegales Verhalten, beispielsweise das Leaken persönlicher Daten oder Gewaltdrohungen.\nDies wird an die Raummoderation gemeldet, welche dies an die Justiz weitergeben kann."
+ "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Diese Person zeigt illegales Verhalten, beispielsweise das Leaken persönlicher Daten oder Gewaltdrohungen.\nDies wird an die Raummoderation gemeldet, welche dies an die Justiz weitergeben kann.",
+ "Unnamed audio": "Unbenannte Audiodatei",
+ "Show %(count)s other previews|one": "%(count)s andere Vorschau zeigen",
+ "Show %(count)s other previews|other": "%(count)s andere Vorschauen zeigen",
+ "Images, GIFs and videos": "Mediendateien",
+ "To view all keyboard shortcuts, click here.": "Alle Tastenkombinationen anzeigen",
+ "Keyboard shortcuts": "Tastenkombinationen",
+ "User %(userId)s is already invited to the room": "%(userId)s ist schon eingeladen",
+ "Unable to copy a link to the room to the clipboard.": "Der Link zum Raum konnte nicht kopiert werden.",
+ "Unable to copy room link": "Raumlink konnte nicht kopiert werden",
+ "Integration manager": "Integrationsverwaltung",
+ "User Directory": "Benutzerverzeichnis",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Dein %(brand)s erlaubt dir nicht, eine Integrationsverwaltung zu verwenden, um dies zu tun. Bitte kontaktiere einen Administrator.",
+ "Copy Link": "Link kopieren",
+ "Transfer Failed": "Übertragen fehlgeschlagen",
+ "Unable to transfer call": "Übertragen des Anrufs fehlgeschlagen",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Wenn du dieses Widget verwendest, können Daten zu %(widgetDomain)s und deinem Integrationsserver übertragen werden.",
+ "Identity server is": "Der Identitätsserver ist",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsverwalter erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Berechtigungslevel setzen.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsverwalter, um Bots, Widgets und Stickerpakete zu verwalten.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsverwalter (%(serverName)s) , um Bots, Widgets und Stickerpakete zu verwalten.",
+ "Identity server": "Identitätsserver",
+ "Identity server (%(server)s)": "Identitätsserver (%(server)s)",
+ "Could not connect to identity server": "Verbindung zum Identitätsserver konnte nicht hergestellt werden",
+ "Not a valid identity server (status code %(code)s)": "Ungültiger Identitätsserver (Fehlercode %(code)s)",
+ "Identity server URL must be HTTPS": "Der Identitätsserver muss über HTTPS erreichbar sein",
+ "Error processing audio message": "Fehler beim Verarbeiten der Audionachricht",
+ "Copy Room Link": "Raumlink kopieren",
+ "Code blocks": "Codeblöcke",
+ "There was an error loading your notification settings.": "Fehler beim Laden der Benachrichtigungseinstellungen.",
+ "Mentions & keywords": "Erwähnungen und Schlüsselwörter",
+ "Global": "Global",
+ "New keyword": "Neues Schlüsselwort",
+ "Keyword": "Schlüsselwort",
+ "Enable email notifications for %(email)s": "E-Mail-Benachrichtigungen für %(email)s aktivieren",
+ "Enable for this account": "Für dieses Konto aktivieren",
+ "An error occurred whilst saving your notification preferences.": "Beim Speichern der Benachrichtigungseinstellungen ist ein Fehler aufgetreten.",
+ "Error saving notification preferences": "Fehler beim Speichern der Benachrichtigungseinstellungen",
+ "Messages containing keywords": "Nachrichten mit Schlüsselwörtern",
+ "Show notification badges for People in Spaces": "Benachrichtigungssymbol für Personen in Spaces zeigen",
+ "Use Ctrl + F to search timeline": "Nutze STRG + F, um den Verlauf zu durchsuchen",
+ "Downloading": "Herunterladen",
+ "The call is in an unknown state!": "Dieser Anruf ist in einem unbekannten Zustand!",
+ "Call back": "Zurückrufen",
+ "You missed this call": "Du hast einen Anruf verpasst",
+ "This call has failed": "Anruf fehlgeschlagen",
+ "Unknown failure: %(reason)s)": "Unbekannter Fehler: %(reason)s",
+ "Connection failed": "Verbindung fehlgeschlagen",
+ "This call has ended": "Anruf beendet",
+ "Connected": "Verbunden",
+ "IRC": "IRC",
+ "Silence call": "Anruf stummschalten",
+ "Error downloading audio": "Fehler beim Herunterladen der Audiodatei",
+ "Image": "Bild",
+ "Sticker": "Sticker",
+ "An unknown error occurred": "Ein unbekannter Fehler ist aufgetreten",
+ "Message bubbles": "Nachrichtenblasen",
+ "New layout switcher (with message bubbles)": "Layout ändern erlauben (mit Nachrichtenblasen)",
+ "New in the Spaces beta": "Neues in der Spaces Beta"
}
diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json
index 8700abbff1..4a485ad7b4 100644
--- a/src/i18n/strings/el.json
+++ b/src/i18n/strings/el.json
@@ -925,5 +925,6 @@
"Done": "Τέλος",
"Not Trusted": "Μη Έμπιστο",
"You're already in a call with this person.": "Είστε ήδη σε κλήση με αυτόν τον χρήστη.",
- "Already in call": "Ήδη σε κλήση"
+ "Already in call": "Ήδη σε κλήση",
+ "Identity server is": "Ο διακομιστής ταυτοποίησης είναι"
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f48f06a791..3ad8daa85c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -35,11 +35,8 @@
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
"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",
"The call was answered on another device.": "The call was answered on another device.",
@@ -55,7 +52,6 @@
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
"No other application is using the webcam": "No other application is using the webcam",
- "Unable to capture screen": "Unable to capture screen",
"VoIP is unsupported": "VoIP is unsupported",
"You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
"Too Many Calls": "Too Many Calls",
@@ -437,8 +433,6 @@
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
"Upgrades a room to a new version": "Upgrades a room to a new version",
"You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.",
- "Error upgrading room": "Error upgrading room",
- "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
"Changes your display nickname": "Changes your display nickname",
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
"Changes the avatar of the current room": "Changes the avatar of the current room",
@@ -493,6 +487,11 @@
"Converts the room to a DM": "Converts the room to a DM",
"Converts the DM to a room": "Converts the DM to a room",
"Displays action": "Displays action",
+ "Someone": "Someone",
+ "%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
+ "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
+ "%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
+ "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s placed a video call. (not supported by this browser)",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepted the invitation for %(displayName)s",
"%(targetName)s accepted an invitation": "%(targetName)s accepted an invitation",
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
@@ -542,7 +541,6 @@
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.",
- "Someone": "Someone",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s made future room history visible to all room members, from the point they joined.",
@@ -719,6 +717,8 @@
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
+ "Error upgrading room": "Error upgrading room",
+ "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space",
"Unknown App": "Unknown App",
@@ -765,6 +765,16 @@
"The person who invited you already left the room.": "The person who invited you already left the room.",
"The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.",
"Failed to join room": "Failed to join room",
+ "New in the Spaces beta": "New in the Spaces beta",
+ "Help people in spaces to find and join private rooms": "Help people in spaces to find and join private rooms",
+ "Learn more": "Learn more",
+ "Help space members find private rooms": "Help space members find private rooms",
+ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "To help space members find and join a private room, go to that room's Security & Privacy settings.",
+ "General": "General",
+ "Security & Privacy": "Security & Privacy",
+ "Roles & Permissions": "Roles & Permissions",
+ "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.",
+ "Skip": "Skip",
"You joined the call": "You joined the call",
"%(senderName)s joined the call": "%(senderName)s joined the call",
"Call in progress": "Call in progress",
@@ -790,12 +800,7 @@
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
- "Show all rooms in Home": "Show all rooms in Home",
- "Show people in spaces": "Show people in spaces",
- "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.",
- "Show notification badges for People in Spaces": "Show notification badges for People in Spaces",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
- "Send and receive voice messages": "Send and receive voice messages",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
"Message Pinning": "Message Pinning",
@@ -864,6 +869,8 @@
"Manually verify all remote sessions": "Manually verify all remote sessions",
"IRC display name width": "IRC display name width",
"Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
+ "Show all rooms in Home": "Show all rooms in Home",
+ "All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading logs": "Uploading logs",
@@ -896,6 +903,10 @@
"You held the call Resume ": "You held the call Resume ",
"%(peerName)s held the call": "%(peerName)s held the call",
"Connecting": "Connecting",
+ "You are presenting": "You are presenting",
+ "%(sharerName)s is presenting": "%(sharerName)s is presenting",
+ "Your camera is turned off": "Your camera is turned off",
+ "Your camera is still enabled": "Your camera is still enabled",
"Video Call": "Video Call",
"Voice Call": "Voice Call",
"Fill Screen": "Fill Screen",
@@ -998,6 +1009,12 @@
"Name": "Name",
"Description": "Description",
"Please enter a name for the space": "Please enter a name for the space",
+ "Spaces are a new feature.": "Spaces are a new feature.",
+ "Spaces feedback": "Spaces feedback",
+ "Thank you for trying Spaces. Your feedback will help inform the next versions.": "Thank you for trying Spaces. Your feedback will help inform the next versions.",
+ "Give feedback.": "Give feedback.",
+ "e.g. my-space": "e.g. my-space",
+ "Address": "Address",
"Create a space": "Create a space",
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.",
"Public": "Public",
@@ -1010,12 +1027,12 @@
"Your private space": "Your private space",
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
"You can change these anytime.": "You can change these anytime.",
- "e.g. my-space": "e.g. my-space",
- "Address": "Address",
"Creating...": "Creating...",
"Create": "Create",
- "All rooms": "All rooms",
"Home": "Home",
+ "Show all rooms": "Show all rooms",
+ "All rooms": "All rooms",
+ "Options": "Options",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
"Click to copy": "Click to copy",
@@ -1025,7 +1042,6 @@
"Invite people": "Invite people",
"Invite with email or username": "Invite with email or username",
"Failed to save space settings.": "Failed to save space settings.",
- "General": "General",
"Edit settings relating to your space.": "Edit settings relating to your space.",
"Saving...": "Saving...",
"Save Changes": "Save Changes",
@@ -1046,16 +1062,9 @@
"Preview Space": "Preview Space",
"Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
"Recommended for public spaces.": "Recommended for public spaces.",
- "Settings": "Settings",
- "Leave space": "Leave space",
- "Create new room": "Create new room",
- "Add existing room": "Add existing room",
- "Members": "Members",
- "Manage & explore rooms": "Manage & explore rooms",
- "Explore rooms": "Explore rooms",
- "Space options": "Space options",
"Expand": "Expand",
"Collapse": "Collapse",
+ "Space options": "Space options",
"Remove": "Remove",
"This bridge was provisioned by .": "This bridge was provisioned by .",
"This bridge is managed by .": "This bridge is managed by .",
@@ -1415,27 +1424,34 @@
"Muted Users": "Muted Users",
"Banned users": "Banned users",
"Send %(eventType)s events": "Send %(eventType)s events",
- "Roles & Permissions": "Roles & Permissions",
"Permissions": "Permissions",
"Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room",
"Enable encryption?": "Enable encryption?",
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption. ": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption. ",
- "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
- "Click here to fix": "Click here to fix",
+ "This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
"To link to this room, please add an address.": "To link to this room, please add an address.",
- "Only people who have been invited": "Only people who have been invited",
- "Anyone who knows the room's link, apart from guests": "Anyone who knows the room's link, apart from guests",
- "Anyone who knows the room's link, including guests": "Anyone who knows the room's link, including guests",
+ "Private (invite only)": "Private (invite only)",
+ "Only invited people can join.": "Only invited people can join.",
+ "Anyone can find and join.": "Anyone can find and join.",
+ "Upgrade required": "Upgrade required",
+ "& %(count)s more|other": "& %(count)s more",
+ "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access",
+ "Anyone in a space can find and join. Edit which spaces can access here. ": "Anyone in a space can find and join. Edit which spaces can access here. ",
+ "Spaces with access": "Spaces with access",
+ "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Anyone in %(spaceName)s can find and join. You can select other spaces too.",
+ "Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.",
+ "Space members": "Space members",
+ "Decide who can join %(roomName)s.": "Decide who can join %(roomName)s.",
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
"Members only (since they were invited)": "Members only (since they were invited)",
"Members only (since they joined)": "Members only (since they joined)",
"Anyone": "Anyone",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.",
+ "People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.",
"Who can read history?": "Who can read history?",
- "Security & Privacy": "Security & Privacy",
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
"Encrypted": "Encrypted",
- "Who can access this room?": "Who can access this room?",
+ "Access": "Access",
"Unable to revoke sharing for email address": "Unable to revoke sharing for email address",
"Unable to share email address": "Unable to share email address",
"Your email address hasn't been verified yet": "Your email address hasn't been verified yet",
@@ -1557,6 +1573,8 @@
"Unnamed room": "Unnamed room",
"World readable": "World readable",
"Guests can join": "Guests can join",
+ "Screen sharing is here!": "Screen sharing is here!",
+ "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!",
"(~%(count)s results)|other": "(~%(count)s results)",
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
@@ -1572,8 +1590,11 @@
"Start chat": "Start chat",
"Rooms": "Rooms",
"Add room": "Add room",
+ "Create new room": "Create new room",
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
+ "Add existing room": "Add existing room",
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
+ "Explore rooms": "Explore rooms",
"Explore community rooms": "Explore community rooms",
"Explore public rooms": "Explore public rooms",
"Low priority": "Low priority",
@@ -1651,6 +1672,7 @@
"Low Priority": "Low Priority",
"Invite People": "Invite People",
"Copy Room Link": "Copy Room Link",
+ "Settings": "Settings",
"Leave Room": "Leave Room",
"Room options": "Room options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
@@ -1744,13 +1766,13 @@
"The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
"Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection",
"Yours, or the other users’ session": "Yours, or the other users’ session",
+ "Members": "Members",
"Nothing pinned, yet": "Nothing pinned, yet",
"If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.",
"Pinned messages": "Pinned messages",
"Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
- "Options": "Options",
"Set my room layout for everyone": "Set my room layout for everyone",
"Widgets": "Widgets",
"Edit widgets, bridges & bots": "Edit widgets, bridges & bots",
@@ -1846,16 +1868,19 @@
"Verification cancelled": "Verification cancelled",
"Compare emoji": "Compare emoji",
"Connected": "Connected",
+ "You declined this call": "You declined this call",
+ "They declined this call": "They declined this call",
+ "Call back": "Call back",
+ "Call again": "Call again",
"This call has ended": "This call has ended",
+ "They didn't pick up": "They didn't pick up",
"Could not connect media": "Could not connect media",
"Connection failed": "Connection failed",
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
"An unknown error occurred": "An unknown error occurred",
- "No answer": "No answer",
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
"This call has failed": "This call has failed",
"You missed this call": "You missed this call",
- "Call back": "Call back",
"The call is in an unknown state!": "The call is in an unknown state!",
"Sunday": "Sunday",
"Monday": "Monday",
@@ -1866,7 +1891,7 @@
"Saturday": "Saturday",
"Today": "Today",
"Yesterday": "Yesterday",
- "Downloading": "Downloading",
+ "Decrypting": "Decrypting",
"Download": "Download",
"View Source": "View Source",
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
@@ -1881,9 +1906,9 @@
"Retry": "Retry",
"Reply": "Reply",
"Message Actions": "Message Actions",
+ "Download %(text)s": "Download %(text)s",
"Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s",
- "Download %(text)s": "Download %(text)s",
"Invalid file%(extra)s": "Invalid file%(extra)s",
"Error decrypting image": "Error decrypting image",
"Show image": "Show image",
@@ -1982,9 +2007,9 @@
"Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages",
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
"This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages",
- "Share your screen": "Share your screen",
- "Screens": "Screens",
- "Windows": "Windows",
+ "Share entire screen": "Share entire screen",
+ "Application window": "Application window",
+ "Share content": "Share content",
"Join": "Join",
"No results": "No results",
"Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.",
@@ -2092,17 +2117,20 @@
"Add a new server...": "Add a new server...",
"%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms",
+ "Add existing space": "Add existing space",
+ "Want to add a new space instead?": "Want to add a new space instead?",
+ "Create a new space": "Create a new space",
+ "Search for spaces": "Search for spaces",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
- "Filter your rooms and spaces": "Filter your rooms and spaces",
- "Feeling experimental?": "Feeling experimental?",
- "You can add existing spaces to a space.": "You can add existing spaces to a space.",
"Direct Messages": "Direct Messages",
"Space selection": "Space selection",
"Add existing rooms": "Add existing rooms",
"Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room",
+ "Search for rooms": "Search for rooms",
+ "Adding spaces has moved.": "Adding spaces has moved.",
"Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID",
"email address": "email address",
@@ -2116,15 +2144,8 @@
"Invite anyway and never warn me again": "Invite anyway and never warn me again",
"Invite anyway": "Invite anyway",
"Close dialog": "Close dialog",
- "Beta feedback": "Beta feedback",
- "Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
- "Done": "Done",
"%(featureName)s beta feedback": "%(featureName)s beta feedback",
- "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
- "Feedback": "Feedback",
- "You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
- "Send feedback": "Send feedback",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.",
"Preparing to send logs": "Preparing to send logs",
"Logs sent": "Logs sent",
@@ -2147,7 +2168,6 @@
"People you know on %(brand)s": "People you know on %(brand)s",
"Hide": "Hide",
"Show": "Show",
- "Skip": "Skip",
"Send %(count)s invites|other": "Send %(count)s invites",
"Send %(count)s invites|one": "Send %(count)s invite",
"Invite people to join %(communityName)s": "Invite people to join %(communityName)s",
@@ -2176,20 +2196,37 @@
"Community ID": "Community ID",
"example": "example",
"Please enter a name for the room": "Please enter a name for the room",
- "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.",
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.",
+ "Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.",
+ "You can change this at any time from room settings.": "You can change this at any time from room settings.",
+ "Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .",
+ "Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.",
+ "Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.",
"You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.",
"Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.",
"Enable end-to-end encryption": "Enable end-to-end encryption",
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.",
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.",
+ "Create a room": "Create a room",
+ "Create a room in %(communityName)s": "Create a room in %(communityName)s",
"Create a public room": "Create a public room",
"Create a private room": "Create a private room",
- "Create a room in %(communityName)s": "Create a room in %(communityName)s",
"Topic (optional)": "Topic (optional)",
- "Make this room public": "Make this room public",
+ "Room visibility": "Room visibility",
+ "Private room (invite only)": "Private room (invite only)",
+ "Public room": "Public room",
+ "Visible to space members": "Visible to space members",
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
"Create Room": "Create Room",
+ "Anyone in will be able to find and join.": "Anyone in will be able to find and join.",
+ "Anyone will be able to find and join this space, not just members of .": "Anyone will be able to find and join this space, not just members of .",
+ "Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.",
+ "Add a space to a space you manage.": "Add a space to a space you manage.",
+ "Space visibility": "Space visibility",
+ "Private space (invite only)": "Private space (invite only)",
+ "Public space": "Public space",
+ "Want to add an existing space instead?": "Want to add an existing space instead?",
+ "Adding...": "Adding...",
"Sign out": "Sign out",
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this",
"You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.",
@@ -2255,8 +2292,10 @@
"Comment": "Comment",
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
"PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.",
+ "Feedback": "Feedback",
"Report a bug": "Report a bug",
"Please view existing bugs on Github first. No match? Start a new one .": "Please view existing bugs on Github first. No match? Start a new one .",
+ "Send feedback": "Send feedback",
"You don't have permission to do this": "You don't have permission to do this",
"Sending": "Sending",
"Sent": "Sent",
@@ -2264,6 +2303,10 @@
"Forward message": "Forward message",
"Message preview": "Message preview",
"Search for rooms or people": "Search for rooms or people",
+ "Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
+ "Done": "Done",
+ "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
+ "You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort",
@@ -2335,12 +2378,33 @@
"Clear cache and resync": "Clear cache and resync",
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating %(brand)s": "Updating %(brand)s",
+ "Leave all rooms and spaces": "Leave all rooms and spaces",
+ "Don't leave any": "Don't leave any",
+ "Leave specific rooms and spaces": "Leave specific rooms and spaces",
+ "Search %(spaceName)s": "Search %(spaceName)s",
+ "You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
+ "You're the only admin of this space. Leaving it will mean no one has control over it.": "You're the only admin of this space. Leaving it will mean no one has control over it.",
+ "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.",
+ "Leave %(spaceName)s": "Leave %(spaceName)s",
+ "Are you sure you want to leave ?": "Are you sure you want to leave ?",
+ "Leave space": "Leave space",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Start using Key Backup": "Start using Key Backup",
"I don't want my encrypted messages": "I don't want my encrypted messages",
"Manually export keys": "Manually export keys",
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
+ "%(count)s members|other": "%(count)s members",
+ "%(count)s members|one": "%(count)s member",
+ "%(count)s rooms|other": "%(count)s rooms",
+ "%(count)s rooms|one": "%(count)s room",
+ "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only",
+ "Select spaces": "Select spaces",
+ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decide which spaces can access this room. If a space is selected, its members can find and join .",
+ "Search spaces": "Search spaces",
+ "Spaces you know that contain this room": "Spaces you know that contain this room",
+ "Other spaces or rooms you might not know": "Other spaces or rooms you might not know",
+ "These are likely ones other room admins are a part of.": "These are likely ones other room admins are a part of.",
"Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:",
"Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:",
"Session name": "Session name",
@@ -2384,12 +2448,13 @@
"Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room",
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room",
"Put a link back to the old room at the start of the new room so people can see old messages": "Put a link back to the old room at the start of the new room so people can see old messages",
- "Automatically invite users": "Automatically invite users",
+ "Automatically invite members from this room to the new one": "Automatically invite members from this room to the new one",
"Upgrade private room": "Upgrade private room",
"Upgrade public room": "Upgrade public room",
"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.": "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.",
"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 .": "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 .",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
+ "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.": "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.",
"You'll upgrade this room from to .": "You'll upgrade this room from to .",
"Resend": "Resend",
"You're all caught up.": "You're all caught up.",
@@ -2412,7 +2477,6 @@
"We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.",
"Other homeserver": "Other homeserver",
"Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
- "Learn more": "Learn more",
"About homeservers": "About homeservers",
"Reset event store?": "Reset event store?",
"You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store",
@@ -2532,6 +2596,8 @@
"Source URL": "Source URL",
"Collapse reply thread": "Collapse reply thread",
"Report": "Report",
+ "Add space": "Add space",
+ "Manage & explore rooms": "Manage & explore rooms",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
@@ -2647,6 +2713,7 @@
"You are an administrator of this community": "You are an administrator of this community",
"You are a member of this community": "You are a member of this community",
"Who can join this community?": "Who can join this community?",
+ "Only people who have been invited": "Only people who have been invited",
"Everyone": "Everyone",
"Your community hasn't got a Long Description, a HTML page to show to community members. Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members. Click here to open settings and give it one!",
"Long Description (HTML)": "Long Description (HTML)",
@@ -2746,10 +2813,6 @@
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
"You don't have permission": "You don't have permission",
- "%(count)s members|other": "%(count)s members",
- "%(count)s members|one": "%(count)s member",
- "%(count)s rooms|other": "%(count)s rooms",
- "%(count)s rooms|one": "%(count)s room",
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Suggested": "Suggested",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
@@ -2767,8 +2830,6 @@
"Search names and descriptions": "Search names and descriptions",
"If you can't find the room you're looking for, ask for an invite or create a new room .": "If you can't find the room you're looking for, ask for an invite or create a new room .",
"Create room": "Create room",
- "Spaces are a beta feature.": "Spaces are a beta feature.",
- "Public space": "Public space",
"Private space": "Private space",
" invites you": " invites you",
"To view %(spaceName)s, turn on the Spaces beta ": "To view %(spaceName)s, turn on the Spaces beta ",
@@ -2783,6 +2844,7 @@
"Creating rooms...": "Creating rooms...",
"What do you want to organise?": "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.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
+ "Search for rooms or spaces": "Search for rooms or spaces",
"Share %(name)s": "Share %(name)s",
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
"Go to my first room": "Go to my first room",
@@ -2794,7 +2856,7 @@
"Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your teammates",
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
- "We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
+ "We're working on this, but just want to let you know.": "We're working on this, but just want to let you know.",
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates",
diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index 41bb44ed83..d70b933e31 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -3326,5 +3326,16 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provu aliajn vortojn aŭ kontorolu, ĉu vi ne tajperaris. Iuj rezultoj eble ne videblos, ĉar ili estas privataj kaj vi bezonus inviton por aliĝi.",
"No results for \"%(query)s\"": "Neniuj rezultoj por «%(query)s»",
"The user you called is busy.": "La uzanto, kiun vi vokis, estas okupata.",
- "User Busy": "Uzanto estas okupata"
+ "User Busy": "Uzanto estas okupata",
+ "Integration manager": "Kunigilo",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Via %(brand)so ne permesas al vi uzi kunigilon por tio. Bonvolu kontakti administranton.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Uzo de tiu ĉi fenestraĵo eble havigos datumojn kun %(widgetDomain)s kaj via kunigilo.",
+ "Identity server is": "Identiga servilo estas",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Kunigiloj ricevas agordajn datumojn, kaj povas modifi fenestraĵojn, sendi invitojn al ĉambroj, kaj vianome agordi povnivelojn.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Uzu kunigilon por administrado de robotoj, fenestraĵoj, kaj glumarkaroj.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Uzu kunigilon (%(serverName)s) por administrado de robotoj, fenestraĵoj, kaj glumarkaroj.",
+ "Identity server": "Identiga servilo",
+ "Identity server (%(server)s)": "Identiga servilo (%(server)s)",
+ "Could not connect to identity server": "Ne povis konektiĝi al identiga servilo",
+ "Not a valid identity server (status code %(code)s)": "Nevalida identiga servilo (statkodo %(code)s)"
}
diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index a06de53821..08a4a2be9a 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -1706,7 +1706,7 @@
"Encrypted by an unverified session": "Cifrado por una sesión no verificada",
"Unencrypted": "Sin cifrar",
"Encrypted by a deleted session": "Cifrado por una sesión eliminada",
- "Invite only": "Sólamente por invitación",
+ "Invite only": "Solo por invitación",
"Scroll to most recent messages": "Ir a los mensajes más recientes",
"Close preview": "Cerrar vista previa",
"No recent messages by %(user)s found": "No se han encontrado mensajes recientes de %(user)s",
@@ -3254,7 +3254,7 @@
"Enter your Security Phrase a second time to confirm it.": "Escribe tu frase de seguridad de nuevo para confirmarla.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elige salas o conversaciones para añadirlas. Este espacio es solo para ti, no informaremos a nadie. Puedes añadir más más tarde.",
"What do you want to organise?": "¿Qué quieres organizar?",
- "Filter all spaces": "Filtrar todos los espacios",
+ "Filter all spaces": "Filtrar espacios",
"%(count)s results in all spaces|one": "%(count)s resultado en todos los espacios",
"%(count)s results in all spaces|other": "%(count)s resultados en todos los espacios",
"You have no ignored users.": "No has ignorado a nadie.",
@@ -3420,5 +3420,62 @@
"%(targetName)s accepted an invitation": "%(targetName)s ha aceptado una invitación",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s ha aceptado la invitación a %(displayName)s",
"We sent the others, but the below people couldn't be invited to ": "Hemos enviado el resto, pero no hemos podido invitar las siguientes personas a la sala ",
- "Some invites couldn't be sent": "No se han podido enviar algunas invitaciones"
+ "Some invites couldn't be sent": "No se han podido enviar algunas invitaciones",
+ "Integration manager": "Gestor de integración",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s no utilizar un \"gestor de integración\" para hacer esto. Por favor, contacta con un administrador.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Usar este widget puede resultar en que se compartan datos con %(widgetDomain)s y su administrador de integración.",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Los administradores de integración reciben datos de configuración, y pueden modificar widgets, enviar invitaciones de sala, y establecer niveles de poder en tu nombre.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Utiliza un administrador de integración para gestionar los bots, los widgets y los paquetes de pegatinas.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usar un gestor de integraciones (%(serverName)s) para manejar los bots, widgets y paquetes de pegatinas.",
+ "Identity server": "Servidor de identidad",
+ "Identity server (%(server)s)": "Servidor de identidad %(server)s",
+ "Could not connect to identity server": "No se ha podido conectar al servidor de identidad",
+ "Not a valid identity server (status code %(code)s)": "No es un servidor de identidad válido (código de estado %(code)s)",
+ "Identity server URL must be HTTPS": "La URL del servidor de identidad debe ser tipo HTTPS",
+ "Unable to copy a link to the room to the clipboard.": "No se ha podido copiar el enlace a la sala.",
+ "Unable to copy room link": "No se ha podido copiar el enlace a la sala",
+ "Unnamed audio": "Audio sin título",
+ "User Directory": "Lista de usuarios",
+ "Error processing audio message": "Error al procesar el mensaje de audio",
+ "Copy Link": "Copiar enlace",
+ "Show %(count)s other previews|one": "Ver otras %(count)s vistas previas",
+ "Show %(count)s other previews|other": "Ver %(count)s otra vista previa",
+ "Images, GIFs and videos": "Imágenes, GIFs y vídeos",
+ "Code blocks": "Bloques de código",
+ "To view all keyboard shortcuts, click here.": "Para ver todos los atajos de teclado, haz clic aquí.",
+ "Keyboard shortcuts": "Atajos de teclado",
+ "Identity server is": "El servidor de identidad es",
+ "There was an error loading your notification settings.": "Ha ocurrido un error al cargar tus ajustes de notificaciones",
+ "Mentions & keywords": "Menciones y palabras clave",
+ "Global": "Global",
+ "New keyword": "Nueva palabra clave",
+ "Keyword": "Palabra clave",
+ "Enable email notifications for %(email)s": "Activar notificaciones por correo electrónico para %(email)s",
+ "Enable for this account": "Activar para esta cuenta",
+ "An error occurred whilst saving your notification preferences.": "Ha ocurrido un error al guardar las tus preferencias de notificaciones.",
+ "Error saving notification preferences": "Error al guardar las preferencias de notificaciones",
+ "Messages containing keywords": "Mensajes que contengan",
+ "Use Command + F to search timeline": "Usa Control + F para buscar",
+ "Transfer Failed": "La transferencia ha fallado",
+ "Unable to transfer call": "No se ha podido transferir la llamada",
+ "This call has ended": "La llamada ha terminado",
+ "Could not connect media": "No se ha podido conectar con los dispositivos multimedia",
+ "Their device couldn't start the camera or microphone": "El dispositivo de la otra persona no ha podido iniciar la cámara o micrófono",
+ "Error downloading audio": "Error al descargar el audio",
+ "Image": "Imagen",
+ "Sticker": "Pegatina",
+ "Downloading": "Descargando",
+ "The call is in an unknown state!": "La llamada está en un estado desconocido",
+ "Call back": "Devolver",
+ "You missed this call": "No has cogido esta llamada",
+ "This call has failed": "Esta llamada ha fallado",
+ "Unknown failure: %(reason)s)": "Fallo desconocido: %(reason)s)",
+ "No answer": "Sin respuesta",
+ "An unknown error occurred": "Ha ocurrido un error desconocido",
+ "Connection failed": "Ha fallado la conexión",
+ "Connected": "Conectado",
+ "Copy Room Link": "Copiar enlace a la sala",
+ "Displaying time": "Mostrando la hora",
+ "IRC": "IRC",
+ "Use Ctrl + F to search timeline": "Usa Control + F para buscar dentro de la conversación"
}
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index ce262233b8..bb7bd575ef 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -1489,7 +1489,7 @@
"Update any local room aliases to point to the new room": "uuendame kõik jututoa aliased nii, et nad viitaks uuele jututoale",
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "ei võimalda kasutajatel enam vanas jututoas suhelda ning avaldame seal teate, mis soovitab kõigil kolida uude jututuppa",
"Put a link back to the old room at the start of the new room so people can see old messages": "selleks et saaks vanu sõnumeid lugeda, paneme uue jututoa algusesse viite vanale jututoale",
- "Automatically invite users": "Kutsu kasutajad automaatselt",
+ "Automatically invite users": "Kutsu automaatselt kasutajaid",
"Upgrade private room": "Uuenda omavaheline jututuba",
"Upgrade public room": "Uuenda avalik jututuba",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Jututoa uuendamine on keerukas toiming ning tavaliselt soovitatakse seda teha vaid siis, kui jututuba on vigade tõttu halvasti kasutatav, sealt on puudu vajalikke funktsionaalsusi või seal ilmneb turvavigu.",
@@ -2829,7 +2829,7 @@
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Sõnumid siin jututoas on läbivalt krüptitud. Klõpsides tunnuspilti saad kontrollida kasutaja %(displayName)s profiili.",
"%(creator)s created this DM.": "%(creator)s alustas seda otsesuhtlust.",
"This is the start of .": "See on jututoa algus.",
- "Add a photo, so people can easily spot your room.": "Selle, et teised märkaks sinu jututuba lihtsamini, palun lisa üks pilt.",
+ "Add a photo, so people can easily spot your room.": "Selleks, et teised märkaks sinu jututuba lihtsamini, palun lisa üks pilt.",
"%(displayName)s created this room.": "%(displayName)s lõi selle jututoa.",
"You created this room.": "Sa lõid selle jututoa.",
"Add a topic to help people know what it is about.": "Selleks, et teised teaks millega on tegemist, palun lisa teema .",
@@ -3450,5 +3450,76 @@
"This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "See kasutaja spämmib jututuba reklaamidega, reklaamlinkidega või propagandaga.\nJututoa moderaatorid saavad selle kohta teate.",
"This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "Selle kasutaja tegevus on äärmiselt ebasobilik, milleks võib olla teiste jututoas osalejate solvamine, peresõbralikku jututuppa täiskasvanutele mõeldud sisu lisamine või muul viisil jututoa reeglite rikkumine.\nJututoa moderaatorid saavad selle kohta teate.",
"Please provide an address": "Palun sisesta aadress",
- "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Meie esimene katsetus modereerimisega. Kui jututoas on modereerimine toetatud, siis „Teata moderaatorile“ nupust võid saada teate ebasobiliku sisu kohta"
+ "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Meie esimene katsetus modereerimisega. Kui jututoas on modereerimine toetatud, siis „Teata moderaatorile“ nupust võid saada teate ebasobiliku sisu kohta",
+ "Unnamed audio": "Nimetu helifail",
+ "Code blocks": "Lähtekoodi lõigud",
+ "Images, GIFs and videos": "Pildid, gif'id ja videod",
+ "Show %(count)s other previews|other": "Näita %(count)s muud eelvaadet",
+ "Show %(count)s other previews|one": "Näita veel %(count)s eelvaadet",
+ "Error processing audio message": "Viga häälsõnumi töötlemisel",
+ "Integration manager": "Lõiminguhaldur",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Sinu %(brand)s ei võimalda selle tegevuse jaoks kasutada lõiminguhaldurit. Palun küsi lisateavet serveri haldajalt.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Selle vidina kasutamisel võidakse jagada andmeid %(widgetDomain)s saitidega ning sinu lõiminguhalduriga.",
+ "Identity server is": "Isikutuvastusserver on",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Lõiminguhalduritel on laiad volitused - nad võivad sinu nimel lugeda seadistusi, kohandada vidinaid, saata jututubade kutseid ning määrata õigusi.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide seadistamiseks kasuta lõiminguhaldurit.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide jaoks kasuta lõiminguhaldurit (%(serverName)s) .",
+ "Identity server": "Isikutuvastusserver",
+ "Identity server (%(server)s)": "Isikutuvastusserver %(server)s",
+ "Could not connect to identity server": "Ei saanud ühendust isikutuvastusserveriga",
+ "Not a valid identity server (status code %(code)s)": "See ei ole sobilik isikutuvastusserver (staatuskood %(code)s)",
+ "Identity server URL must be HTTPS": "Isikutuvastusserveri URL peab kasutama HTTPS-protokolli",
+ "User %(userId)s is already invited to the room": "Kasutaja %(userId)s sai juba kutse sellesse jututuppa",
+ "Use Command + F to search timeline": "Ajajoonelt otsimiseks kasuta Command+F klahve",
+ "Use Ctrl + F to search timeline": "Ajajoonelt otsimiseks kasuta Ctrl+F klahve",
+ "Keyboard shortcuts": "Kiirklahvid",
+ "To view all keyboard shortcuts, click here.": "Vaata siit kõiki kiirklahve.",
+ "Copy Link": "Kopeeri link",
+ "User Directory": "Kasutajate kataloog",
+ "Unable to copy room link": "Jututoa lingi kopeerimine ei õnnestu",
+ "Unable to copy a link to the room to the clipboard.": "Jututoa lingi kopeerimine lõikelauale ei õnnestunud.",
+ "Messages containing keywords": "Sõnumid, mis sisaldavad märksõnu",
+ "Error saving notification preferences": "Viga teavistuste eelistuste salvestamisel",
+ "An error occurred whilst saving your notification preferences.": "Sinu teavituste eelistuste salvestamisel tekkis viga.",
+ "Enable for this account": "Võta sellel kontol kasutusele",
+ "Enable email notifications for %(email)s": "Saada teavitusi %(email)s e-posti aadressile",
+ "Keyword": "Märksõnad",
+ "Mentions & keywords": "Mainimised ja märksõnad",
+ "New keyword": "Uus märksõna",
+ "Global": "Üldised",
+ "There was an error loading your notification settings.": "Sinu teavituste seadistuste laadimisel tekkis viga.",
+ "Transfer Failed": "Edasisuunamine ei õnnestunud",
+ "Unable to transfer call": "Kõne edasisuunamine ei õnnestunud",
+ "Downloading": "Laadin alla",
+ "The call is in an unknown state!": "Selle kõne oleks on teadmata!",
+ "Call back": "Helista tagasi",
+ "This call has failed": "Kõne ühendamine ei õnnestunud",
+ "You missed this call": "Sa ei võtnud kõnet vastu",
+ "Unknown failure: %(reason)s)": "Tundmatu viga: %(reason)s",
+ "No answer": "Keegi ei vasta kõnele",
+ "You're removing all spaces. Access will default to invite only": "Sa oled eemaldamas kõiki kogukonnakeskuseid. Edaspidine ligipääs eeldab kutse olemasolu",
+ "Select spaces": "Vali kogukonnakeskused",
+ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Vali missugustel kogukonnakeskustel on sellele jututoale ligipääs. Kui kogukonnakeskus on valitud, siis selle liikmed saavad jututuba leida ja temaga liituda.",
+ "Search spaces": "Otsi kogukonnakeskusi",
+ "Spaces you know that contain this room": "Sulle teadaolevad kogukonnakeskused, millesse kuulub see jututuba",
+ "Other spaces or rooms you might not know": "Sellised muud jututoad ja kogukonnakeskused, mida sa ei pruugi teada",
+ "Automatically invite members from this room to the new one": "Kutsu jututoa senised liikmed automaatselt uude jututuppa",
+ "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.": "Palun arvesta, et uuendusega tehakse jututoast uus variant . Kõik senised sõnumid jäävad sellesse jututuppa arhiveeritud olekus.",
+ "Only people invited will be able to find and join this room.": "See jututuba on leitav vaid kutse olemasolul ning liitumine on võimalik vaid kutse alusel.",
+ "Create a room": "Loo jututuba",
+ "Private room (invite only)": "Privaatne jututuba (kutse alusel)",
+ "Public room": "Avalik jututuba",
+ "Visible to space members": "Nähtav kogukonnakeskuse liikmetele",
+ "Room visibility": "Jututoa nähtavus",
+ "Spaces with access": "Ligipääsuga kogukonnakeskused",
+ "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Kõik %(spaceName)s kogukonnakeskuse liikmed saavad leida ja liituda. Sa võid valida muid kogukonnakeskuseid.",
+ "Anyone in a space can find and join. You can select multiple spaces.": "Kõik kogukonnakeskuse liikmed saavad leida ja liituda. Sa võid valida ka mitu kogukonnakeskust.",
+ "Space members": "Kogukonnakeskuse liikmed",
+ "Decide who can join %(roomName)s.": "Vali, kes saavad liituda %(roomName)s jututoaga.",
+ "People with supported clients will be able to join the room without having a registered account.": "Kõik kes kasutavad sobilikke klientrakendusi, saavad jututoaga liituda ilma kasutajakonto registreerimiseta.",
+ "Access": "Ligipääs",
+ "The voice message failed to upload.": "Häälsõnumi üleslaadimine ei õnnestunud.",
+ "Everyone in will be able to find and join this room.": "Kõik kogukonna liikmed saavad seda jututuba leida ning võivad temaga liituda.",
+ "You can change this at any time from room settings.": "Sa saad seda alati jututoa seadistustest muuta.",
+ "Anyone will be able to find and join this room, not just members of .": "Mitte ainult kogukonna liikmed, vaid kõik saavad seda jututuba leida ja võivad temaga liituda."
}
diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json
index 2740ea2079..704db34bfd 100644
--- a/src/i18n/strings/eu.json
+++ b/src/i18n/strings/eu.json
@@ -2293,5 +2293,17 @@
"Wrong file type": "Okerreko fitxategi-mota",
"Looks good!": "Itxura ona du!",
"Search rooms": "Bilatu gelak",
- "User menu": "Erabiltzailea-menua"
+ "User menu": "Erabiltzailea-menua",
+ "Integration manager": "Integrazio-kudeatzailea",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Zure %(brand)s aplikazioak ez dizu hau egiteko integrazio kudeatzaile bat erabiltzen uzten. Kontaktatu administratzaileren batekin.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Trepeta hau erabiltzean %(widgetDomain)s domeinuarekin eta zure integrazio kudeatzailearekin datuak partekatu daitezke.",
+ "Identity server is": "Identitate zerbitzaria",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrazio kudeatzaileek konfigurazio datuak jasotzen dituzte, eta trepetak aldatu ditzakete, gelara gonbidapenak bidali, eta botere mailak zure izenean ezarri.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Erabili integrazio kudeatzaile bat botak, trepetak eta eranskailu multzoak kudeatzeko.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Erabili (%(serverName)s) integrazio kudeatzailea botak, trepetak eta eranskailu multzoak kudeatzeko.",
+ "Identity server": "Identitate zerbitzaria",
+ "Identity server (%(server)s)": "Identitate-zerbitzaria (%(server)s)",
+ "Could not connect to identity server": "Ezin izan da identitate-zerbitzarira konektatu",
+ "Not a valid identity server (status code %(code)s)": "Ez da identitate zerbitzari baliogarria (egoera-mezua %(code)s)",
+ "Identity server URL must be HTTPS": "Identitate zerbitzariaren URL-a HTTPS motakoa izan behar du"
}
diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json
index 46dde79945..12c6dd2727 100644
--- a/src/i18n/strings/fa.json
+++ b/src/i18n/strings/fa.json
@@ -3007,5 +3007,19 @@
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here ": "این کار آنها را به %(communityName)s دعوت نمیکند. برای دعوت افراد به %(communityName)s،اینجا کلیک کنید",
"Start a conversation with someone using their name or username (like ).": "با استفاده از نام یا نام کاربری (مانند )، گفتگوی جدیدی را با دیگران شروع کنید.",
"Start a conversation with someone using their name, email address or username (like ).": "با استفاده از نام، آدرس ایمیل و یا نام کاربری (مانند )، یک گفتگوی جدید را شروع کنید.",
- "May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود"
+ "May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود",
+ "Integration manager": "مدیر یکپارچگی",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s شما اجازهٔ استفاده از یک مدیر یکپارچگی را برای این کار نمی دهد. لطفاً با مدیری تماس بگیرید.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "استفاده از این ابزارک ممکن است دادههایی را با %(widgetDomain)s و مدیر یکپارچگیتان هم رسانی کند.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "برای مدیریت باتها، ابزارکها و بستههای برچسب، از یک مدیر پکپارچهسازی استفاده کنید.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "برای مدیریت باتها، ابزارکها و بستههای برچسب، از یک مدیر پکپارچهسازی (%(serverName)s) استفاده کنید.",
+ "Identity server": "کارساز هویت",
+ "Identity server (%(server)s)": "کارساز هویت (%(server)s)",
+ "Could not connect to identity server": "نتوانست به کارساز هویت وصل شود",
+ "Not a valid identity server (status code %(code)s)": "کارساز هویت معتبر نیست (کد وضعیت %(code)s)",
+ "Identity server URL must be HTTPS": "نشانی کارساز هویت باید HTTPS باشد",
+ "Transfer Failed": "انتقال شکست خورد",
+ "Unable to transfer call": "ناتوان در انتقال تماس",
+ "The user you called is busy.": "کاربر موردنظر مشغول است.",
+ "User Busy": "کاربر مشغول"
}
diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json
index 23140846b3..77252f339b 100644
--- a/src/i18n/strings/fi.json
+++ b/src/i18n/strings/fi.json
@@ -3003,5 +3003,17 @@
"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"
+ "%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s",
+ "Integration manager": "Integraatioiden lähde",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-instanssisi ei salli sinun käyttävän integraatioiden lähdettä tämän tekemiseen. Ota yhteys ylläpitäjääsi.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Tämän sovelman käyttäminen saattaa jakaa tietoa osoitteille %(widgetDomain)s ja käyttämällesi integraatioiden lähteelle.",
+ "Identity server is": "Identiteettipalvelin on",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integraatioiden lähteet vastaanottavat asetusdataa ja voivat muokata sovelmia, lähettää kutsuja huoneeseen ja asettaa oikeustasoja puolestasi.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä bottien, sovelmien ja tarrapakettien hallintaan.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä (%(serverName)s) bottien, sovelmien ja tarrapakettien hallintaan.",
+ "Identity server": "Identiteettipalvelin",
+ "Identity server (%(server)s)": "Identiteettipalvelin (%(server)s)",
+ "Could not connect to identity server": "Identiteettipalvelimeen ei saatu yhteyttä",
+ "Not a valid identity server (status code %(code)s)": "Ei kelvollinen identiteettipalvelin (tilakoodi %(code)s)",
+ "Identity server URL must be HTTPS": "Identiteettipalvelimen URL-osoitteen täytyy olla HTTPS-alkuinen"
}
diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 9d047887ba..dec96522b9 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -427,7 +427,7 @@
"You are no longer ignoring %(userId)s": "Vous n’ignorez plus %(userId)s",
"Invite to Community": "Inviter dans la communauté",
"Communities": "Communautés",
- "Message Pinning": "Épingler un message",
+ "Message Pinning": "Messages épinglés",
"Mention": "Mentionner",
"Unignore": "Ne plus ignorer",
"Ignore": "Ignorer",
@@ -3456,5 +3456,42 @@
"%(targetName)s accepted an invitation": "%(targetName)s a accepté une invitation",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s a accepté l’invitation pour %(displayName)s",
"Some invites couldn't be sent": "Certaines invitations n’ont pas pu être envoyées",
- "We sent the others, but the below people couldn't be invited to ": "Nous avons envoyé les invitations, mais les personnes ci-dessous n’ont pas pu être invitées à rejoindre "
+ "We sent the others, but the below people couldn't be invited to ": "Nous avons envoyé les invitations, mais les personnes ci-dessous n’ont pas pu être invitées à rejoindre ",
+ "Integration manager": "Gestionnaire d’intégration",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Votre %(brand)s ne vous autorise pas à utiliser un gestionnaire d’intégrations pour faire ça. Contactez un administrateur.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s et votre gestionnaire d’intégrations.",
+ "Identity server is": "Le serveur d'identité est",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Les gestionnaires d’intégrations reçoivent les données de configuration et peuvent modifier les widgets, envoyer des invitations aux salons et définir les rangs à votre place.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations pour gérer les robots, les widgets et les jeux d’autocollants.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations (%(serverName)s) pour gérer les robots, les widgets et les jeux d’autocollants.",
+ "Identity server": "Serveur d’identité",
+ "Identity server (%(server)s)": "Serveur d’identité (%(server)s)",
+ "Could not connect to identity server": "Impossible de se connecter au serveur d’identité",
+ "Not a valid identity server (status code %(code)s)": "Serveur d’identité non valide (code de statut %(code)s)",
+ "Identity server URL must be HTTPS": "L’URL du serveur d’identité doit être en HTTPS",
+ "User Directory": "Répertoire utilisateur",
+ "Error processing audio message": "Erreur lors du traitement du message audio",
+ "Copy Link": "Copier le lien",
+ "Show %(count)s other previews|one": "Afficher %(count)s autre aperçu",
+ "Show %(count)s other previews|other": "Afficher %(count)s autres aperçus",
+ "Images, GIFs and videos": "Images, GIF et vidéos",
+ "Code blocks": "Blocs de code",
+ "Displaying time": "Affichage de l’heure",
+ "To view all keyboard shortcuts, click here.": "Pour afficher tous les raccourcis clavier, cliquez ici.",
+ "Keyboard shortcuts": "Raccourcis clavier",
+ "There was an error loading your notification settings.": "Une erreur est survenue lors du chargement de vos paramètres de notification.",
+ "Mentions & keywords": "Mentions et mots-clés",
+ "Global": "Global",
+ "New keyword": "Nouveau mot-clé",
+ "Keyword": "Mot-clé",
+ "Enable email notifications for %(email)s": "Activer les notifications par e-mail pour %(email)s",
+ "Enable for this account": "Activer pour ce compte",
+ "An error occurred whilst saving your notification preferences.": "Une erreur est survenue lors de la sauvegarde de vos préférences de notification.",
+ "Error saving notification preferences": "Erreur lors de la sauvegarde des préférences de notification",
+ "Messages containing keywords": "Message contenant les mots-clés",
+ "Use Ctrl + F to search timeline": "Utilisez Ctrl + F pour rechercher dans le fil de discussion",
+ "Use Command + F to search timeline": "Utilisez Commande + F pour rechercher dans le fil de discussion",
+ "User %(userId)s is already invited to the room": "L’utilisateur %(userId)s est déjà invité dans le salon",
+ "Transfer Failed": "Échec du transfert",
+ "Unable to transfer call": "Impossible de transférer l’appel"
}
diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index b880c5b548..856a783497 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -3398,5 +3398,147 @@
"If you have permissions, open the menu on any message and select Pin to stick them here.": "Se tes permisos, abre o menú en calquera mensaxe e elixe Fixar para pegalos aquí.",
"Nothing pinned, yet": "Nada fixado, por agora",
"End-to-end encryption isn't enabled": "Non está activado o cifrado de extremo-a-extremo",
- "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings. ": "As túas mensaxes privadas normalmente están cifradas, pero esta sala non. Habitualmente esto é debido a que se utiliza un dispositivo ou métodos no soportados, como convites por email. Activa o cifrado nos axustes. "
+ "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings. ": "As túas mensaxes privadas normalmente están cifradas, pero esta sala non. Habitualmente esto é debido a que se utiliza un dispositivo ou métodos no soportados, como convites por email. Activa o cifrado nos axustes. ",
+ "Integration manager": "Xestor de Integracións",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "O teu %(brand)s non permite que uses o Xestor de Integracións, contacta coa administración.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Ao utilizar este widget poderías compartir datos con %(widgetDomain)s e o teu Xestor de integracións.",
+ "Identity server is": "O servidor de identidade é",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Os xestores de integracións reciben datos de configuración, e poden modificar os widgets, enviar convites das salas, e establecer roles no teu nome.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integracións para xestionar bots, widgets e paquetes de adhesivos.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integración (%(serverName)s) para xestionar bots, widgets e paquetes de adhesivos.",
+ "Identity server": "Servidor de identidade",
+ "Identity server (%(server)s)": "Servidor de Identidade (%(server)s)",
+ "Could not connect to identity server": "Non hai conexión co Servidor de Identidade",
+ "Not a valid identity server (status code %(code)s)": "Servidor de Identidade non válido (código de estado %(code)s)",
+ "Identity server URL must be HTTPS": "O URL do servidor de identidade debe comezar HTTPS",
+ "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "Esta sala está dedicada a contido ilegal ou tóxico ou a moderación non modera os contidos tóxicos ou ilegais.\nEsto vaise denunciar ante a administración de %(homeserver)s. As administradoras NON poderán ler o contido cifrado desta sala.",
+ "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "Esta usuaria está facendo spam na sala con anuncios, ligazóns a anuncios ou propaganda.\nEsto vai ser denunciado ante a moderación da sala.",
+ "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Esta usuaria está a comportarse dun xeito ilegal, por exemplo ameazando a persoas ou exhibindo violencia.\nEsto vaise denunciar ante a moderación da sala que podería presentar o caso ante as autoridades legais.",
+ "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "Esta usuaria ten un comportamento tóxico, por exemplo insultar a outras usuarias o compartir contido adulto nunha sala de contido familiar ou faltando doutro xeito ás regras desta sala.\nVai ser denunciada ante a moderación da sala.",
+ "What this user is writing is wrong.\nThis will be reported to the room moderators.": "O que escribe esta usuaria non é correcto.\nSerá denunciado á moderación da sala.",
+ "User Directory": "Directorio de Usuarias",
+ "Please provide an address": "Proporciona un enderezo",
+ "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)s cambiou ACLs do servidor",
+ "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)s cambiou o ACLs do servidor %(count)s veces",
+ "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)s cambiaron o ACLs do servidor",
+ "%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)s cambiaron ACLs do servidor %(count)s veces",
+ "Message search initialisation failed, check your settings for more information": "Fallou a inicialización da busca de mensaxes, comproba os axustes para máis información",
+ "Error processing audio message": "Erro ao procesar a mensaxe de audio",
+ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Establecer enderezos para este espazo para que as usuarias poidan atopar o espazo no servidor (%(localDomain)s)",
+ "To publish an address, it needs to be set as a local address first.": "Para publicar un enderezo, primeiro debe establecerse como enderezo local.",
+ "Published addresses can be used by anyone on any server to join your room.": "Os enderezos publicados poden ser utilizados por calquera en calquera servidor para unirse á túa sala.",
+ "Published addresses can be used by anyone on any server to join your space.": "Os enderezos publicados podense usar por calquera en calquera servidor para unirse ao teu espazo.",
+ "This space has no local addresses": "Este espazo non ten enderezos locais",
+ "Copy Link": "Copiar Ligazón",
+ "Show %(count)s other previews|one": "Mostrar %(count)s outra vista previa",
+ "Show %(count)s other previews|other": "Mostrar outras %(count)s vistas previas",
+ "Space information": "Información do Espazo",
+ "Images, GIFs and videos": "Imaxes, GIFs e vídeos",
+ "Code blocks": "Bloques de código",
+ "Displaying time": "Mostrar hora",
+ "To view all keyboard shortcuts, click here.": "Para ver os atallos do teclado preme aquí.",
+ "Keyboard shortcuts": "Atallos de teclado",
+ "There was an error loading your notification settings.": "Houbo un erro ao cargar os axustes de notificación.",
+ "Mentions & keywords": "Mencións e palabras chave",
+ "Global": "Global",
+ "New keyword": "Nova palabra chave",
+ "Keyword": "Palabra chave",
+ "Enable email notifications for %(email)s": "Activar notificacións de email para %(email)s",
+ "Enable for this account": "Activar para esta conta",
+ "An error occurred whilst saving your notification preferences.": "Algo fallou ao gardar as túas preferencias de notificación.",
+ "Error saving notification preferences": "Erro ao gardar os axustes de notificación",
+ "Messages containing keywords": "Mensaxes coas palabras chave",
+ "Collapse": "Pechar",
+ "Expand": "Despregar",
+ "Recommended for public spaces.": "Recomendado para espazos públicos.",
+ "Allow people to preview your space before they join.": "Permitir que sexa visible o espazo antes de unirte a el.",
+ "Preview Space": "Vista previa do Espazo",
+ "only invited people can view and join": "só poden ver e unirse persoas que foron convidadas",
+ "anyone with the link can view and join": "calquera coa ligazón pode ver e unirse",
+ "Decide who can view and join %(spaceName)s.": "Decidir quen pode ver e unirse a %(spaceName)s.",
+ "Visibility": "Visibilidade",
+ "This may be useful for public spaces.": "Esto podería ser útil para espazos públicos.",
+ "Guests can join a space without having an account.": "As convidadas poden unirse ao espazo sen ter unha conta.",
+ "Enable guest access": "Activar acceso de convidadas",
+ "Failed to update the history visibility of this space": "Fallou a actualización da visibilidade do historial do espazo",
+ "Failed to update the guest access of this space": "Fallou a actualización do acceso de convidadas ao espazo",
+ "Failed to update the visibility of this space": "Fallou a actualización da visibilidade do espazo",
+ "Address": "Enderezo",
+ "e.g. my-space": "ex. o-meu-espazo",
+ "Silence call": "Acalar chamada",
+ "Sound on": "Son activado",
+ "Use Ctrl + F to search timeline": "Usar Ctrl + F para buscar na cronoloxía",
+ "Use Command + F to search timeline": "Usar Command + F para buscar na cronoloxía",
+ "Show notification badges for People in Spaces": "Mostra insignia de notificación para Persoas en Espazos",
+ "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "Se está desactivado tamén poderás engadir as Mensaxes Directas aos Espazos personais. Se activado, verás automáticamente quen é membro do Espazo.",
+ "Show people in spaces": "Mostrar persoas nos Espazos",
+ "Show all rooms in Home": "Mostrar tódalas salas no Inicio",
+ "User %(userId)s is already invited to the room": "A usuaria %(userId)s xa ten un convite para a sala",
+ "%(senderName)s changed the pinned messages for the room.": "%(senderName)s cambiou a mensaxe fixada da sala.",
+ "%(senderName)s kicked %(targetName)s": "%(senderName)s expulsou a %(targetName)s",
+ "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s expulsou a %(targetName)s: %(reason)s",
+ "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s retirou o convite para %(targetName)s",
+ "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s retirou o convite para %(targetName)s: %(reason)s",
+ "%(senderName)s unbanned %(targetName)s": "%(senderName)s retiroulle o veto a %(targetName)s",
+ "%(targetName)s left the room": "%(targetName)s saíu da sala",
+ "%(targetName)s left the room: %(reason)s": "%(targetName)s saíu da sala: %(reason)s",
+ "%(targetName)s rejected the invitation": "%(targetName)s rexeitou o convite",
+ "%(targetName)s joined the room": "%(targetName)s uniuse á sala",
+ "%(senderName)s made no change": "%(senderName)s non fixo cambios",
+ "%(senderName)s set a profile picture": "%(senderName)s estableceu a foto de perfil",
+ "%(senderName)s changed their profile picture": "%(senderName)s cambiou a súa foto de perfil",
+ "%(senderName)s removed their profile picture": "%(senderName)s eliminou a súa foto de perfil",
+ "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s eliminou o seu nome público (%(oldDisplayName)s)",
+ "%(senderName)s set their display name to %(displayName)s": "%(senderName)s estableceu o seu nome público como %(displayName)s",
+ "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s cambiou o seu nome público a %(displayName)s",
+ "%(senderName)s banned %(targetName)s": "%(senderName)s vetou %(targetName)s",
+ "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s vetou %(targetName)s: %(reason)s",
+ "%(targetName)s accepted an invitation": "%(targetName)s aceptou o convite",
+ "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s aceptou o convite a %(displayName)s",
+ "Some invites couldn't be sent": "Non se puideron enviar algúns convites",
+ "We sent the others, but the below people couldn't be invited to ": "Convidamos as outras, pero as persoas de aquí embaixo non foron convidadas a ",
+ "Transfer Failed": "Fallou a transferencia",
+ "Unable to transfer call": "Non se puido transferir a chamada",
+ "[number]": "[número]",
+ "To view %(spaceName)s, you need an invite": "Para ver %(spaceName)s precisas un convite",
+ "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Podes premer en calquera momento nun avatar no panel de filtros para ver só salas e persoas asociadas con esa comunidade.",
+ "Unable to copy a link to the room to the clipboard.": "Non se copiou a ligazón da sala ao portapapeis.",
+ "Unable to copy room link": "Non se puido copiar ligazón da sala",
+ "Unnamed audio": "Audio sen nome",
+ "Move down": "Ir abaixo",
+ "Move up": "Ir arriba",
+ "Report": "Denunciar",
+ "Collapse reply thread": "Contraer fío de resposta",
+ "Show preview": "Ver vista previa",
+ "View source": "Ver fonte",
+ "Forward": "Reenviar",
+ "Settings - %(spaceName)s": "Axustes - %(spaceName)s",
+ "Report the entire room": "Denunciar a toda a sala",
+ "Spam or propaganda": "Spam ou propaganda",
+ "Illegal Content": "Contido ilegal",
+ "Toxic Behaviour": "Comportamento tóxico",
+ "Disagree": "En desacordo",
+ "Please pick a nature and describe what makes this message abusive.": "Escolle unha opción e describe a razón pola que esta é unha mensaxe abusiva.",
+ "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Outra razón. Por favor, describe o problema.\nInformaremos disto á moderación da sala.",
+ "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "Esta sala está dedicada a contido tóxico ou ilegal ou a moderación non é quen de moderar contido ilegal ou tóxico.\nImos informar disto á administración de %(homeserver)s.",
+ "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Modelo de denuncia ante a moderación. Nas salas que teñen moderación, o botón `denuncia`permíteche denunciar un abuso á moderación da sala",
+ "Copy Room Link": "Copiar Ligazón da sala",
+ "Downloading": "Descargando",
+ "The call is in an unknown state!": "Esta chamada ten un estado descoñecido!",
+ "Call back": "Devolver a chamada",
+ "You missed this call": "Perdeches esta chamada",
+ "This call has failed": "A chamada fallou",
+ "Unknown failure: %(reason)s)": "Fallo descoñecido: %(reason)s",
+ "No answer": "Sen resposta",
+ "An unknown error occurred": "Aconteceu un fallo descoñecido",
+ "Their device couldn't start the camera or microphone": "O seu dispositivo non puido acender a cámara ou micrófono",
+ "Connection failed": "Fallou a conexión",
+ "Could not connect media": "Non se puido conectar o multimedia",
+ "This call has ended": "A chamada rematou",
+ "Connected": "Conectado",
+ "Message bubbles": "Burbullas con mensaxes",
+ "IRC": "IRC",
+ "New layout switcher (with message bubbles)": "Nova disposición do control (con burbullas con mensaxes)",
+ "Image": "Imaxe",
+ "Sticker": "Adhesivo"
}
diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json
index 5baa1d7c67..31859b712c 100644
--- a/src/i18n/strings/he.json
+++ b/src/i18n/strings/he.json
@@ -2097,7 +2097,7 @@
"You do not have permission to create rooms in this community.": "אין לך הרשאה ליצור חדרים בקהילה זו.",
"Cannot create rooms in this community": "לא ניתן ליצור חדרים בקהילה זו",
"Failed to reject invitation": "דחיית ההזמנה נכשלה",
- "Explore rooms": "שיטוט בחדרים",
+ "Explore rooms": "גלה חדרים",
"Create a Group Chat": "צור צ'אט קבוצתי",
"Explore Public Rooms": "חקור חדרים ציבוריים",
"Send a Direct Message": "שלח הודעה ישירה",
@@ -2785,5 +2785,20 @@
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "לא ניתן היה להגיע לשרת הבית שלך ולא היה ניתן להתחבר. נסה שוב. אם זה נמשך, אנא פנה למנהל שרת הבית שלך.",
"Try again": "נסה שוב",
"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.": "ביקשנו מהדפדפן לזכור באיזה שרת בית אתה משתמש כדי לאפשר לך להיכנס, אך למרבה הצער הדפדפן שלך שכח אותו. עבור לדף הכניסה ונסה שוב.",
- "We couldn't log you in": "לא הצלחנו להתחבר אליך"
+ "We couldn't log you in": "לא הצלחנו להתחבר אליך",
+ "Integration manager": "מנהל אינטגרציה",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s שלכם אינו מאפשר לך להשתמש במנהל שילוב לשם כך. אנא צרו קשר עם מנהל מערכת.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "שימוש ביישומון זה עשוי לשתף נתונים עם %(widgetDomain)s ומנהל האינטגרציה שלך.",
+ "Identity server is": "שרת ההזדהות הינו",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "מנהלי שילוב מקבלים נתוני תצורה ויכולים לשנות ווידג'טים, לשלוח הזמנות לחדר ולהגדיר רמות הספק מטעמכם.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "השתמש במנהל שילוב לניהול בוטים, ווידג'טים וחבילות מדבקות.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "השתמש במנהל שילוב (%(serverName)s) לניהול בוטים, ווידג'טים וחבילות מדבקות.",
+ "Identity server": "שרת הזדהות",
+ "Identity server (%(server)s)": "שרת הזדהות (%(server)s)",
+ "Could not connect to identity server": "לא ניתן להתחבר אל שרת הזיהוי",
+ "Not a valid identity server (status code %(code)s)": "שרת זיהוי לא מאושר(קוד סטטוס %(code)s)",
+ "Identity server URL must be HTTPS": "הזיהוי של כתובת השרת חייבת להיות מאובטחת ב- HTTPS",
+ "Enter Security Phrase": "הזן ביטוי אבטחה",
+ "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "לא ניתן לפענח גיבוי עם ביטוי אבטחה זה: אנא ודא שהזנת את ביטוי האבטחה הנכון.",
+ "Incorrect Security Phrase": "ביטוי אבטחה שגוי"
}
diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json
index f71c024342..eb0da42ae5 100644
--- a/src/i18n/strings/hi.json
+++ b/src/i18n/strings/hi.json
@@ -588,5 +588,6 @@
"The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।",
"Explore rooms": "रूम का अन्वेषण करें",
"Sign In": "साइन करना",
- "Create Account": "खाता बनाएं"
+ "Create Account": "खाता बनाएं",
+ "Identity server is": "आइडेंटिटी सर्वर हैं"
}
diff --git a/src/i18n/strings/hr.json b/src/i18n/strings/hr.json
index 8070757426..abf903be63 100644
--- a/src/i18n/strings/hr.json
+++ b/src/i18n/strings/hr.json
@@ -205,5 +205,8 @@
"Add Email Address": "Dodaj email adresu",
"Confirm": "Potvrdi",
"Click the button below to confirm adding this email address.": "Kliknite gumb ispod da biste potvrdili dodavanje ove email adrese.",
- "Confirm adding email": "Potvrdite dodavanje email adrese"
+ "Confirm adding email": "Potvrdite dodavanje email adrese",
+ "Integration manager": "Upravitelj integracijama",
+ "Identity server": "Poslužitelj identiteta",
+ "Could not connect to identity server": "Nije moguće spojiti se na poslužitelja identiteta"
}
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 683f825187..55adba87b2 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -3476,5 +3476,29 @@
"Address": "Cím",
"e.g. my-space": "pl. én-terem",
"Silence call": "Némít",
- "Sound on": "Hang be"
+ "Sound on": "Hang be",
+ "Use Command + F to search timeline": "Command + F az idővonalon való kereséshez",
+ "Unnamed audio": "Névtelen hang",
+ "Error processing audio message": "Hiba a hangüzenet feldolgozásánál",
+ "Show %(count)s other previews|one": "%(count)s további előnézet megjelenítése",
+ "Show %(count)s other previews|other": "%(count)s további előnézet megjelenítése",
+ "Images, GIFs and videos": "Képek, GIFek és videók",
+ "Code blocks": "Kód blokkok",
+ "Displaying time": "Idő megjelenítése",
+ "To view all keyboard shortcuts, click here.": "A billentyűzet kombinációk megjelenítéséhez kattintson ide.",
+ "Keyboard shortcuts": "Billentyűzet kombinációk",
+ "Use Ctrl + F to search timeline": "Ctrl + F az idővonalon való kereséshez",
+ "User %(userId)s is already invited to the room": "%(userId)s felhasználó már kapott meghívót a szobába",
+ "Integration manager": "Integrációs Menedzser",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "A %(brand)sod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg a(z) %(widgetDomain)s oldallal és az Integrációkezelővel.",
+ "Identity server is": "Azonosítási szerver",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet beállíthatja helyetted.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert (%(serverName)s) a botok, kisalkalmazások és matrica csomagok kezeléséhez.",
+ "Identity server": "Azonosító szerver",
+ "Identity server (%(server)s)": "Azonosítási kiszolgáló (%(server)s)",
+ "Could not connect to identity server": "Az Azonosítási Szerverhez nem lehet csatlakozni",
+ "Not a valid identity server (status code %(code)s)": "Az Azonosítási Szerver nem érvényes (státusz kód: %(code)s)",
+ "Identity server URL must be HTTPS": "Az Azonosítási Szerver URL-jének HTTPS-nek kell lennie"
}
diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json
index 2de350bae3..b6ec8e2fa6 100644
--- a/src/i18n/strings/id.json
+++ b/src/i18n/strings/id.json
@@ -279,5 +279,9 @@
"A call is currently being placed!": "Sedang melakukan panggilan sekarang!",
"A call is already in progress!": "Masih ada panggilan berlangsung!",
"Permission Required": "Permisi Dibutuhkan",
- "You do not have permission to start a conference call in this room": "Anda tidak memiliki permisi untuk memulai panggilan massal di ruang ini"
+ "You do not have permission to start a conference call in this room": "Anda tidak memiliki permisi untuk memulai panggilan massal di ruang ini",
+ "Explore rooms": "Jelajahi ruang",
+ "Sign In": "Masuk",
+ "Create Account": "Buat Akun",
+ "Identity server": "Server Identitas"
}
diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json
index e8718c941a..a20a30cb52 100644
--- a/src/i18n/strings/is.json
+++ b/src/i18n/strings/is.json
@@ -728,5 +728,7 @@
"Explore all public rooms": "Kanna öll almenningsherbergi",
"Liberate your communication": "Frelsaðu samskipti þín",
"Welcome to ": "Velkomin til ",
- "Welcome to %(appName)s": "Velkomin til %(appName)s"
+ "Welcome to %(appName)s": "Velkomin til %(appName)s",
+ "Identity server is": "Auðkennisþjónn er",
+ "Identity server": "Auðkennisþjónn"
}
diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 2d98072f78..20116411d5 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -564,7 +564,7 @@
"Click to mute audio": "Clicca per silenziare l'audio",
"Clear filter": "Annulla filtro",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Si è tentato di caricare un punto specifico nella cronologia della stanza, ma non hai l'autorizzazione per vedere il messaggio in questione.",
- "Tried to load a specific point in this room's timeline, but was unable to find it.": "Si è tentato di caricare un punto specifico nella cronologia della stanza, ma non si è trovato.",
+ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Si è tentato di caricare un punto specifico nella cronologia della stanza, ma non è stato trovato.",
"Failed to load timeline position": "Caricamento posizione cronologica fallito",
"Uploading %(filename)s and %(count)s others|other": "Invio di %(filename)s e altri %(count)s",
"Uploading %(filename)s and %(count)s others|zero": "Invio di %(filename)s",
@@ -1266,7 +1266,7 @@
"Sends the given message coloured as a rainbow": "Invia il messaggio dato colorato come un arcobaleno",
"Sends the given emote coloured as a rainbow": "Invia l'emoticon dato colorato come un arcobaleno",
"The user's homeserver does not support the version of the room.": "L'homeserver dell'utente non supporta la versione della stanza.",
- "Show hidden events in timeline": "Mostra eventi nascosti nella timeline",
+ "Show hidden events in timeline": "Mostra eventi nascosti nella linea temporale",
"When rooms are upgraded": "Quando le stanze vengono aggiornate",
"this room": "questa stanza",
"View older messages in %(roomName)s.": "Vedi messaggi più vecchi in %(roomName)s.",
@@ -1485,7 +1485,7 @@
"Error changing power level requirement": "Errore nella modifica del livello dei permessi",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "C'é stato un errore nel cambio di libelli dei permessi. Assicurati di avere i permessi necessari e riprova.",
"No recent messages by %(user)s found": "Non sono stati trovati messaggi recenti dell'utente %(user)s",
- "Try scrolling up in the timeline to see if there are any earlier ones.": "Prova a scorrere la timeline per vedere se ce ne sono di precedenti.",
+ "Try scrolling up in the timeline to see if there are any earlier ones.": "Prova a scorrere la linea temporale per vedere se ce ne sono di precedenti.",
"Remove recent messages by %(user)s": "Rimuovi gli ultimi messaggi di %(user)s",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Stai per rimuovere %(count)s messaggi di %(user)s. L'azione é irreversibile. Vuoi continuare?",
"For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Se i messaggi sono tanti può volerci un po' di tempo. Nel frattempo, per favore, non fare alcun refresh.",
@@ -2043,7 +2043,7 @@
"Collapse room list section": "Riduci sezione elenco stanze",
"Expand room list section": "Espandi sezione elenco stanze",
"Clear room list filter field": "Svuota campo filtri elenco stanze",
- "Scroll up/down in the timeline": "Scorri su/giù nella cronologia",
+ "Scroll up/down in the timeline": "Scorri su/giù nella linea temporale",
"Toggle the top left menu": "Attiva/disattiva menu in alto a sinistra",
"Close dialog or context menu": "Chiudi finestra o menu contestuale",
"Activate selected button": "Attiva pulsante selezionato",
@@ -3481,5 +3481,107 @@
"%(senderName)s changed their profile picture": "%(senderName)s ha cambiato la propria immagine del profilo",
"%(senderName)s removed their profile picture": "%(senderName)s ha rimosso la propria immagine del profilo",
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s ha rimosso il proprio nome (%(oldDisplayName)s)",
- "%(senderName)s set their display name to %(displayName)s": "%(senderName)s ha impostato il proprio nome a %(displayName)s"
+ "%(senderName)s set their display name to %(displayName)s": "%(senderName)s ha impostato il proprio nome a %(displayName)s",
+ "Integration manager": "Gestore di integrazioni",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Il tuo %(brand)s non ti permette di usare il gestore di integrazioni per questa azione. Contatta un amministratore.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s e il tuo gestore di integrazioni.",
+ "Identity server is": "Il server di identità è",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "I gestori di integrazione ricevono dati di configurazione e possono modificare widget, inviare inviti alla stanza, assegnare permessi a tuo nome.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni per gestire bot, widget e pacchetti di adesivi.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni (%(serverName)s) per gestire bot, widget e pacchetti di adesivi.",
+ "Identity server": "Server di identità",
+ "Identity server (%(server)s)": "Server di identità (%(server)s)",
+ "Could not connect to identity server": "Impossibile connettersi al server di identità",
+ "Not a valid identity server (status code %(code)s)": "Non è un server di identità valido (codice di stato %(code)s)",
+ "Identity server URL must be HTTPS": "L'URL del server di identità deve essere HTTPS",
+ "Unable to copy a link to the room to the clipboard.": "Impossibile copiare un collegamento alla stanza negli appunti.",
+ "Unable to copy room link": "Impossibile copiare il link della stanza",
+ "Unnamed audio": "Audio senza nome",
+ "Error processing audio message": "Errore elaborazione messaggio audio",
+ "Copy Link": "Copia collegamento",
+ "Show %(count)s other previews|one": "Mostra %(count)s altra anteprima",
+ "Show %(count)s other previews|other": "Mostra altre %(count)s anteprime",
+ "Images, GIFs and videos": "Immagini, GIF e video",
+ "Code blocks": "Blocchi di codice",
+ "To view all keyboard shortcuts, click here.": "Per vedere tutte le scorciatoie, clicca qui.",
+ "Keyboard shortcuts": "Scorciatoie da tastiera",
+ "There was an error loading your notification settings.": "Si è verificato un errore caricando le tue impostazioni di notifica.",
+ "Mentions & keywords": "Menzioni e parole chiave",
+ "Global": "Globale",
+ "New keyword": "Nuova parola chiave",
+ "Keyword": "Parola chiave",
+ "Enable email notifications for %(email)s": "Attive le notifiche email per %(email)s",
+ "Enable for this account": "Attiva per questo account",
+ "An error occurred whilst saving your notification preferences.": "Si è verificato un errore durante il salvataggio delle tue preferenze di notifica.",
+ "Error saving notification preferences": "Errore nel salvataggio delle preferenze di notifica",
+ "Messages containing keywords": "Messaggi contenenti parole chiave",
+ "Use Ctrl + F to search timeline": "Usa Ctrl + F per cercare nella linea temporale",
+ "Use Command + F to search timeline": "Usa Command + F per cercare nella linea temporale",
+ "User %(userId)s is already invited to the room": "L'utente %(userId)s è già stato invitato nella stanza",
+ "Transfer Failed": "Trasferimento fallito",
+ "Unable to transfer call": "Impossibile trasferire la chiamata",
+ "New layout switcher (with message bubbles)": "Nuovo commutatore disposizione (con nuvolette dei messaggi)",
+ "Error downloading audio": "Errore di scaricamento dell'audio",
+ "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.": "Nota che aggiornare creerà una nuova versione della stanza . Tutti i messaggi attuali resteranno in questa stanza archiviata.",
+ "Automatically invite members from this room to the new one": "Invita automaticamente i membri da questa stanza a quella nuova",
+ "These are likely ones other room admins are a part of.": "Questi sono probabilmente quelli di cui fanno parte gli altri amministratori delle stanze.",
+ "Other spaces or rooms you might not know": "Altri spazi o stanze che potresti non conoscere",
+ "Spaces you know that contain this room": "Spazi di cui sai che contengono questa stanza",
+ "Search spaces": "Cerca spazi",
+ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decidi quali spazi possono accedere a questa stanza. Se uno spazio è selezionato, i suoi membri possono trovare ed entrare in .",
+ "Select spaces": "Seleziona spazi",
+ "You're removing all spaces. Access will default to invite only": "Stai rimuovendo tutti gli spazi. L'accesso tornerà solo su invito",
+ "User Directory": "Elenco utenti",
+ "Room visibility": "Visibilità stanza",
+ "Visible to space members": "Visibile ai membri dello spazio",
+ "Public room": "Stanza pubblica",
+ "Private room (invite only)": "Stanza privata (solo a invito)",
+ "Create a room": "Crea una stanza",
+ "Only people invited will be able to find and join this room.": "Solo le persone invitate potranno trovare ed entrare in questa stanza.",
+ "Anyone will be able to find and join this room, not just members of .": "Chiunque potrà trovare ed entrare in questa stanza, non solo i membri di .",
+ "You can change this at any time from room settings.": "Puoi cambiarlo in qualsiasi momento dalle impostazioni della stanza.",
+ "Everyone in will be able to find and join this room.": "Chiunque in potrà trovare ed entrare in questa stanza.",
+ "Image": "Immagine",
+ "Sticker": "Sticker",
+ "Downloading": "Scaricamento",
+ "The call is in an unknown state!": "La chiamata è in uno stato sconosciuto!",
+ "Call back": "Richiama",
+ "You missed this call": "Hai perso questa chiamata",
+ "This call has failed": "Questa chiamata è fallita",
+ "Unknown failure: %(reason)s)": "Errore sconosciuto: %(reason)s)",
+ "No answer": "Nessuna risposta",
+ "An unknown error occurred": "Si è verificato un errore sconosciuto",
+ "Their device couldn't start the camera or microphone": "Il suo dispositivo non ha potuto avviare la fotocamera o il microfono",
+ "Connection failed": "Connessione fallita",
+ "Could not connect media": "Connessione del media fallita",
+ "This call has ended": "Questa chiamata è terminata",
+ "Connected": "Connesso",
+ "The voice message failed to upload.": "Invio del messaggio vocale fallito.",
+ "Copy Room Link": "Copia collegamento stanza",
+ "Access": "Accesso",
+ "People with supported clients will be able to join the room without having a registered account.": "Le persone con client supportati potranno entrare nella stanza senza avere un account registrato.",
+ "Decide who can join %(roomName)s.": "Decidi chi può entrare in %(roomName)s.",
+ "Space members": "Membri dello spazio",
+ "Anyone in a space can find and join. You can select multiple spaces.": "Chiunque in uno spazio può trovare ed entrare. Puoi selezionare più spazi.",
+ "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Chiunque in %(spaceName)s può trovare ed entrare. Puoi selezionare anche altri spazi.",
+ "Spaces with access": "Spazi con accesso",
+ "Anyone in a space can find and join. Edit which spaces can access here. ": "Chiunque in uno spazio può trovare ed entrare. Modifica quali spazi possono accedere qui. ",
+ "Currently, %(count)s spaces have access|other": "Attualmente, %(count)s spazi hanno accesso",
+ "& %(count)s more|other": "e altri %(count)s",
+ "Upgrade required": "Aggiornamento necessario",
+ "Anyone can find and join.": "Chiunque può trovare ed entrare.",
+ "Only invited people can join.": "Solo le persone invitate possono entrare.",
+ "Private (invite only)": "Privato (solo a invito)",
+ "This upgrade will allow members of selected spaces access to this room without an invite.": "Questo aggiornamento permetterà ai membri di spazi selezionati di accedere alla stanza senza invito.",
+ "Message bubbles": "Messaggi",
+ "IRC": "IRC",
+ "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Ciò rende facile mantenere private le stanze in uno spazio, mentre le persone potranno trovarle ed unirsi. Tutte le stanze nuove in uno spazio avranno questa opzione disponibile.",
+ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Per aiutare i membri dello spazio a trovare ed entrare in una stanza privata, vai nelle impostazioni \"Sicurezza e privacy\" di quella stanza.",
+ "Help space members find private rooms": "Aiuta i membri dello spazio a trovare stanze private",
+ "Help people in spaces to find and join private rooms": "Aiuta le persone negli spazi a trovare ed entrare nelle stanze private",
+ "New in the Spaces beta": "Novità nella beta degli spazi",
+ "They didn't pick up": "Non ha risposto",
+ "Call again": "Richiama",
+ "They declined this call": "Ha rifiutato questa chiamata",
+ "You declined this call": "Hai rifiutato questa chiamata"
}
diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index e395c51254..6f18b8e384 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -2504,5 +2504,15 @@
"You can change these anytime.": "ここで入力した情報はいつでも編集できます。",
"Add some details to help people recognise it.": "情報を入力してください。",
"View dev tools": "開発者ツールを表示",
- "To view %(spaceName)s, you need an invite": "%(spaceName)s を閲覧するには招待が必要です"
+ "To view %(spaceName)s, you need an invite": "%(spaceName)s を閲覧するには招待が必要です",
+ "Integration manager": "インテグレーションマネージャ",
+ "Identity server is": "アイデンティティ・サーバー",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "インテグレーションマネージャは設定データを受け取り、ユーザーの代わりにウィジェットの変更、部屋への招待の送信、権限レベルの設定を行うことができます。",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "インテグレーションマネージャを使用して、ボット、ウィジェット、ステッカーパックを管理します。",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "インテグレーションマネージャ (%(serverName)s) を使用して、ボット、ウィジェット、ステッカーパックを管理します。",
+ "Identity server": "認証サーバ",
+ "Identity server (%(server)s)": "identity サーバー (%(server)s)",
+ "Could not connect to identity server": "identity サーバーに接続できませんでした",
+ "Not a valid identity server (status code %(code)s)": "有効な identity サーバーではありません (ステータスコード %(code)s)",
+ "Identity server URL must be HTTPS": "identityサーバーのURLは HTTPS スキーマである必要があります"
}
diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json
index b6e1b3020f..3a7daa3b4c 100644
--- a/src/i18n/strings/kab.json
+++ b/src/i18n/strings/kab.json
@@ -2754,5 +2754,17 @@
"(an error occurred)": "(tella-d tuccḍa)",
"(connection failed)": "(tuqqna ur teddi ara)",
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Iqeddcen akk ttwagedlen seg uttekki! Taxxamt-a dayen ur tettuseqdac ara.",
- "Try again": "Ɛreḍ tikkelt-nniḍen"
+ "Try again": "Ɛreḍ tikkelt-nniḍen",
+ "Integration manager": "Amsefrak n umsidef",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-ik·im ur ak·am yefki ara tisirag i useqdec n umsefrak n umsidef i wakken ad tgeḍ aya. Ttxil-k·m nermes anedbal.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Aseqdec n uwiǧit-a yezmer ad yebḍu isefka d %(widgetDomain)s & amsefrak-inek·inem n umsidef.",
+ "Identity server is": "Aqeddac n timagit d",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Imsefrak n yimsidaf remmsen-d isefka n uswel, syen ad uɣalen zemren ad beddlen iwiǧiten, ad aznen tinubgiwin ɣer texxamin, ad yesbadu daɣen tazmert n yiswiren s yiswiren deg ubdil-ik·im.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Seqdec amsefrak n umsidef i usefrek n yibuten, n yiwiǧiten d tɣawsiwin n usenteḍ.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Seqdec amsefrak n umsidef (%(serverName)s) i usefrek n yibuten, n yiwiǧiten d tɣawsiwin n usenteḍ.",
+ "Identity server": "Aqeddac n timagit",
+ "Identity server (%(server)s)": "Aqeddac n timagit (%(server)s)",
+ "Could not connect to identity server": "Ur izmir ara ad yeqqen ɣer uqeddac n timagit",
+ "Not a valid identity server (status code %(code)s)": "Aqeddac n timagit mačči d ameɣtu (status code %(code)s)",
+ "Identity server URL must be HTTPS": "URL n uqeddac n timagit ilaq ad yili d HTTPS"
}
diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json
index f817dbc26b..c6e48d2a58 100644
--- a/src/i18n/strings/ko.json
+++ b/src/i18n/strings/ko.json
@@ -1667,5 +1667,13 @@
"The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "사용자 %(userId)s의 세션 %(deviceId)s에서 받은 서명 키와 당신이 제공한 서명 키가 일치합니다. 세션이 검증되었습니다.",
"Show more": "더 보기",
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "비밀번호를 변경한다면 방의 암호화 키를 내보낸 후 다시 가져오지 않는 이상 모든 종단간 암호화 키는 초기화 될 것이고, 암호화된 대화 내역은 읽을 수 없게 될 것입니다. 이 문제는 추후에 개선될 것입니다.",
- "Create Account": "계정 만들기"
+ "Create Account": "계정 만들기",
+ "Integration manager": "통합 관리자",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "이 위젯을 사용하면 %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.",
+ "Identity server is": "ID 서버:",
+ "Identity server": "ID 서버",
+ "Identity server (%(server)s)": "ID 서버 (%(server)s)",
+ "Could not connect to identity server": "ID 서버에 연결할 수 없음",
+ "Not a valid identity server (status code %(code)s)": "올바르지 않은 ID 서버 (상태 코드 %(code)s)",
+ "Identity server URL must be HTTPS": "ID 서버 URL은 HTTPS이어야 함"
}
diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json
index 4449ef97c2..870396cd4c 100644
--- a/src/i18n/strings/lt.json
+++ b/src/i18n/strings/lt.json
@@ -2421,5 +2421,17 @@
"New Zealand": "Naujoji Zelandija",
"New Caledonia": "Naujoji Kaledonija",
"Netherlands": "Nyderlandai",
- "Cayman Islands": "Kaimanų Salos"
+ "Cayman Islands": "Kaimanų Salos",
+ "Integration manager": "Integracijų tvarkytuvas",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Jūsų %(brand)s neleidžia jums naudoti integracijų tvarkytuvo tam atlikti. Susisiekite su administratoriumi.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Naudojimasis šiuo valdikliu gali pasidalinti duomenimis su %(widgetDomain)s ir jūsų integracijų tvarkytuvu.",
+ "Identity server is": "Tapatybės serveris yra",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integracijų Tvarkytuvai gauna konfigūracijos duomenis ir jūsų vardu gali keisti valdiklius, siųsti kambario pakvietimus ir nustatyti galios lygius.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Naudokite Integracijų Tvarkytuvą botų, valdiklių ir lipdukų pakuočių tvarkymui.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Naudokite Integracijų Tvarkytuvą (%(serverName)s) botų, valdiklių ir lipdukų pakuočių tvarkymui.",
+ "Identity server": "Tapatybės serveris",
+ "Identity server (%(server)s)": "Tapatybės serveris (%(server)s)",
+ "Could not connect to identity server": "Nepavyko prisijungti prie tapatybės serverio",
+ "Not a valid identity server (status code %(code)s)": "Netinkamas tapatybės serveris (statuso kodas %(code)s)",
+ "Identity server URL must be HTTPS": "Tapatybės Serverio URL privalo būti HTTPS"
}
diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json
index b56599f26e..00c140c0a9 100644
--- a/src/i18n/strings/lv.json
+++ b/src/i18n/strings/lv.json
@@ -1582,5 +1582,9 @@
"Upload files": "Failu augšupielāde",
"These files are too large to upload. The file size limit is %(limit)s.": "Šie faili pārsniedz augšupielādes izmēra limitu %(limit)s.",
"Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)",
- "Check your devices": "Pārskatiet savas ierīces"
+ "Check your devices": "Pārskatiet savas ierīces",
+ "Integration manager": "Integrācija pārvaldnieks",
+ "Identity server is": "Indentifikācijas serveris ir",
+ "Identity server": "Identitāšu serveris",
+ "Could not connect to identity server": "Neizdevās pieslēgties identitāšu serverim"
}
diff --git a/src/i18n/strings/ml.json b/src/i18n/strings/ml.json
index 6183fe7de2..0aee8b5581 100644
--- a/src/i18n/strings/ml.json
+++ b/src/i18n/strings/ml.json
@@ -130,5 +130,7 @@
"Checking for an update...": "അപ്ഡേറ്റ് ഉണ്ടോ എന്ന് തിരയുന്നു...",
"Explore rooms": "മുറികൾ കണ്ടെത്തുക",
"Sign In": "പ്രവേശിക്കുക",
- "Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക"
+ "Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക",
+ "Integration manager": "സംയോജക മാനേജർ",
+ "Identity server": "തിരിച്ചറിയൽ സെർവർ"
}
diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json
index d3be9cd2ea..0ea13d1a1b 100644
--- a/src/i18n/strings/nb_NO.json
+++ b/src/i18n/strings/nb_NO.json
@@ -1981,5 +1981,13 @@
"Costa Rica": "Costa Rica",
"Cook Islands": "Cook-øyene",
"All keys backed up": "Alle nøkler er sikkerhetskopiert",
- "Secret storage:": "Hemmelig lagring:"
+ "Secret storage:": "Hemmelig lagring:",
+ "Integration manager": "Integreringsbehandler",
+ "Identity server is": "Identitetstjeneren er",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integreringsbehandlere mottar oppsettsdata, og kan endre på moduler, sende rominvitasjoner, og bestemme styrkenivåer på dine vegne.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Bruk en integreringsbehandler til å behandle botter, moduler, og klistremerkepakker.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Bruk en integreringsbehandler (%(serverName)s) til å behandle botter, moduler, og klistremerkepakker.",
+ "Identity server": "Identitetstjener",
+ "Identity server (%(server)s)": "Identitetstjener (%(server)s)",
+ "Could not connect to identity server": "Kunne ikke koble til identitetsserveren"
}
diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 72168eb5ff..54cd4c6f66 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -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 uitgelogd. Gelieve opnieuw inloggen.",
+ "For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie uitgelogd. Log opnieuw in.",
"%(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",
@@ -1034,12 +1034,12 @@
"Legal": "Juridisch",
"Credits": "Met dank aan",
"For help with using %(brand)s, click here .": "Klik hier voor hulp bij het gebruiken van %(brand)s.",
- "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Klik hier voor hulp bij het gebruiken van %(brand)s, of begin een gesprek met onze robot met de knop hieronder.",
+ "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Klik hier voor hulp bij het gebruiken van %(brand)s of begin een gesprek met onze robot met de knop hieronder.",
"Help & About": "Hulp & info",
"Bug reporting": "Bug meldingen",
"FAQ": "FAQ",
"Versions": "Versies",
- "Preferences": "Instellingen",
+ "Preferences": "Voorkeuren",
"Composer": "Opsteller",
"Timeline": "Tijdslijn",
"Room list": "Gesprekslijst",
@@ -1199,7 +1199,7 @@
"Invalid homeserver discovery response": "Ongeldig homeserver-vindbaarheids-antwoord",
"Invalid identity server discovery response": "Ongeldig identiteitsserver-vindbaarheidsantwoord",
"General failure": "Algemene fout",
- "This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met e-mailadres.",
+ "This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met een e-mailadres.",
"Please contact your service administrator to continue using this service.": "Gelieve contact op te nemen met uw dienstbeheerder 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",
@@ -1272,7 +1272,7 @@
"Upload files (%(current)s of %(total)s)": "Bestanden versturen (%(current)s van %(total)s)",
"Upload files": "Bestanden versturen",
"Upload": "Versturen",
- "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is te groot om te versturen. De bestandsgroottelimiet is %(limit)s, maar dit bestand is %(sizeOfThisFile)s.",
+ "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is te groot om te versturen. Het limiet is %(limit)s en dit bestand is %(sizeOfThisFile)s.",
"These files are too large to upload. The file size limit is %(limit)s.": "Deze bestanden zijn te groot om te versturen. De bestandsgroottelimiet is %(limit)s.",
"Some files are too large to be uploaded. The file size limit is %(limit)s.": "Sommige bestanden zijn te groot om te versturen. De bestandsgroottelimiet is %(limit)s.",
"Upload %(count)s other files|other": "%(count)s overige bestanden versturen",
@@ -1402,7 +1402,7 @@
"Summary": "Samenvatting",
"Sign in and regain access to your account.": "Meld u aan en herkrijg toegang tot uw account.",
"You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt niet inloggen met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.",
- "This account has been deactivated.": "Deze account is gesloten.",
+ "This account has been deactivated.": "Dit account is gesloten.",
"Messages": "Berichten",
"Actions": "Acties",
"Displays list of commands with usages and descriptions": "Toont een lijst van beschikbare opdrachten, met hun gebruiken en beschrijvingen",
@@ -1497,7 +1497,7 @@
"Share this email in Settings to receive invites directly in %(brand)s.": "Deel in de instellingen dit e-mailadres om uitnodigingen direct in %(brand)s te ontvangen.",
"Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings .": "Gebruik een identiteitsserver om uit te nodigen op e-mailadres. Gebruik de standaardserver (%(defaultIdentityServerName)s) of beheer de server in de Instellingen .",
"Use an identity server to invite by email. Manage in Settings .": "Gebruik een identiteitsserver om anderen uit te nodigen via e-mail. Beheer de server in de Instellingen .",
- "Please fill why you're reporting.": "Gelieve aan te geven waarom u deze melding indient.",
+ "Please fill why you're reporting.": "Geef aan waarom u deze melding indient.",
"Report Content to Your Homeserver Administrator": "Inhoud melden aan de beheerder van uw homeserver",
"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.": "Dit bericht melden zal zijn unieke ‘gebeurtenis-ID’ versturen naar de beheerder van uw homeserver. Als de berichten in dit gesprek versleuteld zijn, zal de beheerder van uw homeserver het bericht niet kunnen lezen, noch enige bestanden of afbeeldingen zien.",
"Send report": "Rapport versturen",
@@ -1564,7 +1564,7 @@
"Session already verified!": "Sessie al geverifieerd!",
"WARNING: Session already verified, but keys do NOT MATCH!": "PAS OP: de sessie is al geverifieerd, maar de sleutels komen NIET OVEREEN!",
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "PAS OP: sleutelverificatie MISLUKT! De combinatie %(userId)s + sessie %(deviceId)s is ondertekend met ‘%(fprint)s’ - maar de opgegeven sleutel is ‘%(fingerprint)s’. Wellicht worden uw berichten onderschept!",
- "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "De door u verschafte en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.",
+ "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "De door u verschafte sleutel en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.",
"%(senderName)s placed a voice call.": "%(senderName)s probeert u te bellen.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s poogt u te bellen, maar uw browser ondersteunt dat niet",
"%(senderName)s placed a video call.": "%(senderName)s doet een video-oproep.",
@@ -2333,7 +2333,7 @@
"Switch to dark mode": "Naar donkere modus wisselen",
"Switch to light mode": "Naar lichte modus wisselen",
"Appearance": "Weergave",
- "All settings": "Alle instellingen",
+ "All settings": "Instellingen",
"Error removing address": "Fout bij verwijderen van adres",
"There was an error removing that address. It may no longer exist or a temporary error occurred.": "Er is een fout opgetreden bij het verwijderen van dit adres. Deze bestaat mogelijk niet meer, of er is een tijdelijke fout opgetreden.",
"You don't have permission to delete the address.": "U heeft geen toestemming om het adres te verwijderen.",
@@ -2517,7 +2517,7 @@
"You can only pin up to %(count)s widgets|other": "U kunt maar %(count)s widgets vastzetten",
"In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "In versleutelde gesprekken zijn uw berichten beveiligd, enkel de ontvanger en u hebben de unieke sleutels om ze te ontsleutelen.",
"Waiting for you to accept on your other session…": "Wachten totdat u uw uitnodiging in uw andere sessie aanneemt…",
- "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Stel een adres in zodat gebruikers dit gesprek via uw homeserver (%(localDomain)s) kunnen vinden",
+ "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Stel een adres in zodat personen dit gesprek via uw homeserver (%(localDomain)s) kunnen vinden",
"Local Addresses": "Lokale adressen",
"Local address": "Lokaal adres",
"The server has denied your request.": "De server heeft uw verzoek afgewezen.",
@@ -2564,7 +2564,7 @@
"Use app for a better experience": "Gebruik de app voor een betere ervaring",
"Enable desktop notifications": "Bureaubladmeldingen inschakelen",
"Don't miss a reply": "Mis geen antwoord",
- "Unknown App": "Onbekende App",
+ "Unknown App": "Onbekende app",
"Error leaving room": "Fout bij verlaten gesprek",
"Unexpected server error trying to leave the room": "Onverwachte serverfout bij het verlaten van dit gesprek",
"See %(msgtype)s messages posted to your active room": "Zie %(msgtype)s -berichten verstuurd in uw actieve gesprek",
@@ -2803,7 +2803,7 @@
"Successfully restored %(sessionCount)s keys": "Succesvol %(sessionCount)s sleutels hersteld",
"Keys restored": "Sleutels hersteld",
"Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Back-up kon niet worden ontsleuteld met dit veiligheidswachtwoord: controleer of u het juiste veiligheidswachtwoord hebt ingevoerd.",
- "Incorrect Security Phrase": "Onjuist Veiligheidswachtwoord",
+ "Incorrect Security Phrase": "Onjuist veiligheidswachtwoord",
"Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Back-up kon niet worden ontcijferd met deze veiligheidssleutel: controleer of u de juiste veiligheidssleutel hebt ingevoerd.",
"Security Key mismatch": "Verkeerde veiligheidssleutel",
"%(completed)s of %(total)s keys restored": "%(completed)s van %(total)s sleutels hersteld",
@@ -3021,9 +3021,9 @@
"Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Uw bericht is niet verstuurd, omdat deze homeserver is geblokkeerd door zijn beheerder. Gelieve contact op te nemen met uw beheerder om de dienst te blijven gebruiken.",
"Are you sure you want to leave the space '%(spaceName)s'?": "Weet u zeker dat u de space '%(spaceName)s' wilt verlaten?",
"This space is not public. You will not be able to rejoin without an invite.": "Deze space is niet openbaar. Zonder uitnodiging zult u niet opnieuw kunnen toetreden.",
- "Start audio stream": "Audio-stream starten",
+ "Start audio stream": "Audiostream starten",
"Failed to start livestream": "Starten van livestream is mislukt",
- "Unable to start audio streaming.": "Kan audio-streaming niet starten.",
+ "Unable to start audio streaming.": "Kan audiostream niet starten.",
"Save Changes": "Wijzigingen opslaan",
"Saving...": "Opslaan...",
"View dev tools": "Bekijk dev tools",
@@ -3034,7 +3034,7 @@
"Edit settings relating to your space.": "Bewerk instellingen gerelateerd aan uw space.",
"Invite someone using their name, username (like ) or share this space .": "Nodig iemand uit per naam, gebruikersnaam (zoals ) of deel deze space .",
"Invite someone using their name, email address, username (like ) or share this space .": "Nodig iemand uit per naam, e-mailadres, gebruikersnaam (zoals ) of deel deze space .",
- "Unnamed Space": "Naamloze Space",
+ "Unnamed Space": "Naamloze space",
"Invite to %(spaceName)s": "Voor %(spaceName)s uitnodigen",
"Failed to add rooms to space": "Het toevoegen van gesprekken aan de space is mislukt",
"Apply": "Toepassen",
@@ -3158,7 +3158,7 @@
"%(count)s people you know have already joined|other": "%(count)s personen die u kent hebben zijn al geregistreerd",
"Accept on your other login…": "Accepteer op uw andere login…",
"Stop & send recording": "Stop & verstuur opname",
- "Record a voice message": "Audiobericht opnemen",
+ "Record a voice message": "Spraakbericht opnemen",
"Invite messages are hidden by default. Click to show the message.": "Uitnodigingen zijn standaard verborgen. Klik om de uitnodigingen weer te geven.",
"Quick actions": "Snelle acties",
"Invite to just this room": "Uitnodigen voor alleen dit gesprek",
@@ -3176,7 +3176,7 @@
"If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.",
"Only do this if you have no other device to complete verification with.": "Doe dit alleen als u geen ander apparaat hebt om de verificatie mee uit te voeren.",
"Reset everything": "Alles opnieuw instellen",
- "Forgotten or lost all recovery methods? Reset all ": "Alles vergeten of alle herstelmethoden verloren? Alles opnieuw instellen ",
+ "Forgotten or lost all recovery methods? Reset all ": "Alles vergeten en alle herstelmethoden verloren? Alles opnieuw instellen ",
"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": "Als u dat doet, let wel geen van uw berichten wordt verwijderd, maar de zoekresultaten zullen gedurende enkele ogenblikken verslechteren terwijl de index opnieuw wordt aangemaakt",
"View message": "Bericht bekijken",
"Zoom in": "Inzoomen",
@@ -3369,5 +3369,108 @@
"%(targetName)s accepted an invitation": "%(targetName)s accepteerde de uitnodiging",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepteerde de uitnodiging voor %(displayName)s",
"Some invites couldn't be sent": "Sommige uitnodigingen konden niet verstuurd worden",
- "We sent the others, but the below people couldn't be invited to ": "De anderen zijn verstuurd, maar de volgende mensen konden niet worden uitgenodigd voor "
+ "We sent the others, but the below people couldn't be invited to ": "De anderen zijn verstuurd, maar de volgende mensen konden niet worden uitgenodigd voor ",
+ "Unnamed audio": "Naamloze audio",
+ "Error processing audio message": "Fout bij verwerking audiobericht",
+ "Show %(count)s other previews|one": "%(count)s andere preview weergeven",
+ "Show %(count)s other previews|other": "%(count)s andere previews weergeven",
+ "Images, GIFs and videos": "Afbeeldingen, GIF's en video's",
+ "Code blocks": "Codeblokken",
+ "Displaying time": "Tijdsweergave",
+ "To view all keyboard shortcuts, click here.": "Om alle sneltoetsen te zien, klik hier.",
+ "Keyboard shortcuts": "Sneltoetsen",
+ "Use Ctrl + F to search timeline": "Gebruik Ctrl +F om te zoeken in de tijdlijn",
+ "Use Command + F to search timeline": "Gebruik Command + F om te zoeken in de tijdlijn",
+ "User %(userId)s is already invited to the room": "De gebruiker %(userId)s is al uitgenodigd voor dit gesprek",
+ "Integration manager": "Integratiebeheerder",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Uw %(brand)s laat u geen integratiebeheerder gebruiken om dit te doen. Neem contact op met een beheerder.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Met het gebruik van deze widget deelt u mogelijk gegevens met %(widgetDomain)s & uw integratiebeheerder.",
+ "Identity server is": "Identiteitsserver is",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integratiebeheerders ontvangen configuratie-informatie en kunnen widgets aanpassen, gespreksuitnodigingen versturen en machtsniveau’s namens u aanpassen.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder om bots, widgets en stickerpakketten te beheren.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder (%(serverName)s) om bots, widgets en stickerpakketten te beheren.",
+ "Identity server": "Identiteitsserver",
+ "Identity server (%(server)s)": "Identiteitsserver (%(server)s)",
+ "Could not connect to identity server": "Kon geen verbinding maken met de identiteitsserver",
+ "Not a valid identity server (status code %(code)s)": "Geen geldige identiteitsserver (statuscode %(code)s)",
+ "Identity server URL must be HTTPS": "Identiteitsserver-URL moet HTTPS zijn",
+ "User Directory": "Gebruikersgids",
+ "Copy Link": "Link kopieren",
+ "There was an error loading your notification settings.": "Er was een fout bij het laden van uw meldingsvoorkeuren.",
+ "Mentions & keywords": "Vermeldingen & trefwoorden",
+ "Global": "Overal",
+ "New keyword": "Nieuw trefwoord",
+ "Keyword": "Trefwoord",
+ "Enable email notifications for %(email)s": "E-mailmeldingen inschakelen voor %(email)s",
+ "Enable for this account": "Voor dit account inschakelen",
+ "An error occurred whilst saving your notification preferences.": "Er is een fout opgetreden tijdens het opslaan van uw meldingsvoorkeuren.",
+ "Error saving notification preferences": "Fout bij het opslaan van meldingsvoorkeuren",
+ "Messages containing keywords": "Berichten met trefwoord",
+ "Transfer Failed": "Doorverbinden is mislukt",
+ "Unable to transfer call": "Doorverbinden is mislukt",
+ "Unable to copy a link to the room to the clipboard.": "Kopiëren van gesprekslink naar het klembord is mislukt.",
+ "Unable to copy room link": "Kopiëren van gesprekslink is mislukt",
+ "Copy Room Link": "Kopieer gesprekslink",
+ "Message bubbles": "Berichtenbubbels",
+ "IRC": "IRC",
+ "New layout switcher (with message bubbles)": "Nieuwe layout schakelaar (met berichtenbubbels)",
+ "Downloading": "Downloading",
+ "The call is in an unknown state!": "Deze oproep heeft een onbekende status!",
+ "Call back": "Terugbellen",
+ "You missed this call": "U heeft deze oproep gemist",
+ "This call has failed": "Deze oproep is mislukt",
+ "Unknown failure: %(reason)s)": "Onbekende fout: %(reason)s",
+ "No answer": "Geen antwoord",
+ "An unknown error occurred": "Er is een onbekende fout opgetreden",
+ "Their device couldn't start the camera or microphone": "Het andere apparaat kon de camera of microfoon niet starten",
+ "Connection failed": "Verbinding mislukt",
+ "Could not connect media": "Mediaverbinding mislukt",
+ "This call has ended": "Deze oproep is beëindigd",
+ "Connected": "Verbonden",
+ "Spaces with access": "Spaces met toegang",
+ "Anyone in a space can find and join. Edit which spaces can access here. ": "Iedereen in een space kan het gesprek vinden en aan deelnemen. Wijzig welke spaces toegang hebben hier. ",
+ "Currently, %(count)s spaces have access|other": "Momenteel hebben %(count)s spaces toegang",
+ "& %(count)s more|other": "& %(count)s meer",
+ "Upgrade required": "Upgrade noodzakelijk",
+ "Anyone can find and join.": "Iedereen kan hem vinden en deelnemen.",
+ "Only invited people can join.": "Alleen uitgenodigde personen kunnen deelnemen.",
+ "Private (invite only)": "Privé (alleen op uitnodiging)",
+ "This upgrade will allow members of selected spaces access to this room without an invite.": "Deze upgrade maakt het mogelijk voor leden van geselecteerde spaces om toegang te krijgen tot dit gesprek zonder een uitnodiging.",
+ "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Dit maakt het makkelijk om gesprekken privé te houden voor een space, terwijl personen in de space hem kunnen vinden en aan deelnemen. Alle nieuwe gesprekken in deze space hebben deze optie beschikbaar.",
+ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Om space leden te helpen met het vinden van en deel te nemen aan privégesprekken, ga naar uw gespreksinstellingen voor veiligheid & privacy.",
+ "Help space members find private rooms": "Help space leden privégesprekken te vinden",
+ "Help people in spaces to find and join private rooms": "Help personen in spaces om privégesprekken te vinden en aan deel te nemen",
+ "New in the Spaces beta": "Nieuw in de spaces beta",
+ "Everyone in will be able to find and join this room.": "Iedereen in kan dit gesprek vinden en aan deelnemen.",
+ "Image": "Afbeelding",
+ "Sticker": "Sticker",
+ "They didn't pick up": "Ze hebben niet opgenomen",
+ "Call again": "Opnieuw bellen",
+ "They declined this call": "Ze weigerden deze oproep",
+ "You declined this call": "U heeft deze oproep geweigerd",
+ "The voice message failed to upload.": "Het spraakbericht versturen is mislukt.",
+ "Access": "Toegang",
+ "People with supported clients will be able to join the room without having a registered account.": "Personen met geschikte apps zullen aan de gesprekken kunnen deelnemen zonder een account te hebben.",
+ "Decide who can join %(roomName)s.": "Kies wie kan deelnemen aan %(roomName)s.",
+ "Space members": "Space leden",
+ "Anyone in a space can find and join. You can select multiple spaces.": "Iedereen in een space kan zoeken en deelnemen. U kunt meerdere spaces selecteren.",
+ "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Iedereen in %(spaceName)s kan zoeken en deelnemen. U kunt ook andere spaces selecteren.",
+ "Visible to space members": "Zichtbaar voor space leden",
+ "Public room": "Openbaar gesprek",
+ "Private room (invite only)": "Privégesprek (alleen op uitnodiging)",
+ "Create a room": "Gesprek aanmaken",
+ "Only people invited will be able to find and join this room.": "Alleen uitgenodigde personen kunnen dit gesprek vinden en aan deelnemen.",
+ "Anyone will be able to find and join this room, not just members of .": "Iedereen kan dit gesprek vinden en aan deelnemen, niet alleen leden van .",
+ "You can change this at any time from room settings.": "U kan dit op elk moment wijzigen vanuit de gespreksinstellingen.",
+ "Error downloading audio": "Fout bij downloaden van audio",
+ "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.": "Let op bijwerken maakt een nieuwe versie van dit gesprek . Alle huidige berichten blijven in dit gearchiveerde gesprek.",
+ "Automatically invite members from this room to the new one": "Automatisch leden uitnodigen van dit gesprek in de nieuwe",
+ "These are likely ones other room admins are a part of.": "Er zijn waarschijnlijk gesprekken waar andere gespreksbeheerders deel van uitmaken.",
+ "Other spaces or rooms you might not know": "Andere spaces of gesprekken die u misschien niet kent",
+ "Spaces you know that contain this room": "Spaces die u kent met dit gesprek",
+ "Search spaces": "Spaces zoeken",
+ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Kies welke spaces toegang hebben tot dit gesprek. Als een space is geselecteerd kunnen deze leden vinden en aan deelnemen.",
+ "Select spaces": "Spaces selecteren",
+ "You're removing all spaces. Access will default to invite only": "U verwijderd alle spaces. De toegang zal standaard alleen op uitnodiging zijn",
+ "Room visibility": "Gesprekszichtbaarheid"
}
diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json
index 478f05b5cb..f53b092d5f 100644
--- a/src/i18n/strings/nn.json
+++ b/src/i18n/strings/nn.json
@@ -1376,5 +1376,12 @@
"Identity Server": "Identitetstenar",
"Email Address": "E-postadresse",
"Go Back": "Gå attende",
- "Notification settings": "Varslingsinnstillingar"
+ "Notification settings": "Varslingsinnstillingar",
+ "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Du bør fjerne dine personlege data frå identitetstenaren før du koplar frå. Dessverre er identitetstenaren utilgjengeleg og kan ikkje nåast akkurat no.",
+ "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Vi tilrår at du slettar personleg informasjon, som e-postadresser og telefonnummer frå identitetstenaren før du koplar frå.",
+ "Privacy": "Personvern",
+ "Versions": "Versjonar",
+ "Legal": "Juridisk",
+ "Identity server is": "Identitetstenaren er",
+ "Identity server": "Identitetstenar"
}
diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 784307acff..524d73eeca 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -2368,5 +2368,15 @@
"Some suggestions may be hidden for privacy.": "Niektóre propozycje mogą być ukryte z uwagi na prywatność.",
"If you can't see who you’re looking for, send them your invite link below.": "Jeżeli nie możesz zobaczyć osób, których szukasz, wyślij im poniższy odnośnik z zaproszeniem.",
"Or send invite link": "Lub wyślij odnośnik z zaproszeniem",
- "We're working on this as part of the beta, but just want to let you know.": "Pracujemy nad tym w ramach bety, ale chcemy, żebyś wiedział(a)."
+ "We're working on this as part of the beta, but just want to let you know.": "Pracujemy nad tym w ramach bety, ale chcemy, żebyś wiedział(a).",
+ "Integration manager": "Menedżer Integracji",
+ "Identity server is": "Serwer tożsamości to",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Zarządcy integracji otrzymują dane konfiguracji, mogą modyfikować widżety, wysyłać zaproszenia do pokoi i ustawiać poziom uprawnień w Twoim imieniu.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji aby zarządzać botami, widżetami i pakietami naklejek.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji %(serverName)s aby zarządzać botami, widżetami i pakietami naklejek.",
+ "Identity server": "Serwer toższamości",
+ "Identity server (%(server)s)": "Serwer tożsamości (%(server)s)",
+ "Could not connect to identity server": "Nie można połączyć z serwerem tożsamości",
+ "Not a valid identity server (status code %(code)s)": "Nieprawidłowy serwer tożsamości (kod statusu %(code)s)",
+ "Identity server URL must be HTTPS": "URL serwera tożsamości musi być HTTPS"
}
diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json
index 4047aae760..32984092e4 100644
--- a/src/i18n/strings/pt.json
+++ b/src/i18n/strings/pt.json
@@ -572,5 +572,7 @@
"Your user agent": "O seu user agent",
"Explore rooms": "Explorar rooms",
"Sign In": "Iniciar sessão",
- "Create Account": "Criar conta"
+ "Create Account": "Criar conta",
+ "Not a valid identity server (status code %(code)s)": "Servidor de Identidade inválido (código de status %(code)s)",
+ "Identity server URL must be HTTPS": "O link do servidor de identidade deve começar com HTTPS"
}
diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index e19febd6ef..7b45b30f4b 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -3110,5 +3110,17 @@
"Inviting...": "Convidando...",
"Invite by username": "Convidar por nome de usuário",
"Support": "Suporte",
- "Original event source": "Fonte do evento original"
+ "Original event source": "Fonte do evento original",
+ "Integration manager": "Gerenciador de integrações",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Seu %(brand)s não permite que você use o gerenciador de integrações para fazer isso. Entre em contato com o administrador.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Se você usar esse widget, os dados poderão ser compartilhados com %(widgetDomain)s & seu gerenciador de integrações.",
+ "Identity server is": "O servidor de identificação é",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "O gerenciador de integrações recebe dados de configuração e pode modificar widgets, enviar convites para salas e definir níveis de permissão em seu nome.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Use o gerenciador de integrações para gerenciar bots, widgets e pacotes de figurinhas.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use o gerenciador de integrações em (%(serverName)s) para gerenciar bots, widgets e pacotes de figurinhas.",
+ "Identity server": "Servidor de identidade",
+ "Identity server (%(server)s)": "Servidor de identidade (%(server)s)",
+ "Could not connect to identity server": "Não foi possível conectar-se ao servidor de identidade",
+ "Not a valid identity server (status code %(code)s)": "Servidor de identidade inválido (código de status %(code)s)",
+ "Identity server URL must be HTTPS": "O link do servidor de identidade deve começar com HTTPS"
}
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 91b9919d0a..e562ce074b 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -3219,5 +3219,17 @@
"Send and receive voice messages": "Отправлять и получать голосовые сообщения",
"%(deviceId)s from %(ip)s": "%(deviceId)s с %(ip)s",
"The user you called is busy.": "Вызываемый пользователь занят.",
- "User Busy": "Пользователь занят"
+ "User Busy": "Пользователь занят",
+ "Integration manager": "Менеджер интеграции",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Ваш %(brand)s не позволяет вам использовать для этого Менеджер Интеграции. Пожалуйста, свяжитесь с администратором.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Используя этот виджет, вы можете делиться данными с %(widgetDomain)s и вашим Менеджером Интеграции.",
+ "Identity server is": "Сервер идентификации",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджеры интеграции получают данные конфигурации и могут изменять виджеты, отправлять приглашения в комнаты и устанавливать уровни доступа от вашего имени.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Используйте Менеджер интеграциями для управления ботами, виджетами и стикерами.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Используйте менеджер интеграций %(serverName)s для управления ботами, виджетами и стикерами.",
+ "Identity server": "Сервер идентификаций",
+ "Identity server (%(server)s)": "Сервер идентификации (%(server)s)",
+ "Could not connect to identity server": "Не смог подключиться к серверу идентификации",
+ "Not a valid identity server (status code %(code)s)": "Неправильный Сервер идентификации (код статуса %(code)s)",
+ "Identity server URL must be HTTPS": "URL-адрес сервера идентификации должен быть HTTPS"
}
diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json
index 0ee0c6cbc3..d8902a3784 100644
--- a/src/i18n/strings/sk.json
+++ b/src/i18n/strings/sk.json
@@ -2080,5 +2080,14 @@
"The call was answered on another device.": "Hovor bol prijatý na inom zariadení.",
"The call could not be established": "Hovor nemohol byť realizovaný",
"The other party declined the call.": "Druhá strana odmietla hovor.",
- "Call Declined": "Hovor odmietnutý"
+ "Call Declined": "Hovor odmietnutý",
+ "Integration manager": "Správca integrácií",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integračné servery zhromažďujú údaje nastavení, môžu spravovať widgety, odosielať vo vašom mene pozvánky alebo meniť úroveň moci.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Použiť integračný server na správu botov, widgetov a balíčkov s nálepkami.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Použiť integračný server (%(serverName)s) na správu botov, widgetov a balíčkov s nálepkami.",
+ "Identity server": "Server totožností",
+ "Identity server (%(server)s)": "Server totožností (%(server)s)",
+ "Could not connect to identity server": "Nie je možné sa pripojiť k serveru totožností",
+ "Not a valid identity server (status code %(code)s)": "Toto nie je funkčný server totožností (kód stavu %(code)s)",
+ "Identity server URL must be HTTPS": "URL adresa servera totožností musí začínať HTTPS"
}
diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index e6f27a955d..45f76a4a61 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -3447,7 +3447,7 @@
"Show all rooms in Home": "Shfaq krejt dhomat te Home",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Prototip “Njoftojuani moderatorëve”. Në dhoma që mbulojnë moderim, butoni `raportojeni` do t’ju lejojë t’u njoftoni abuzim moderatorëve të dhomës",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s ndryshoi mesazhin e fiksuar për këtë dhomë.",
- "%(senderName)s kicked %(targetName)s": "%(senderName)s përzuri %(targetName)s.",
+ "%(senderName)s kicked %(targetName)s": "%(senderName)s përzuri %(targetName)s",
"%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s përzuri %(targetName)s: %(reason)s",
"%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s tërhoqi mbrapsht ftesën për %(targetName)s",
"%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s tërhoqi mbrapsht ftesën për %(targetName)s: %(reason)s",
@@ -3460,13 +3460,113 @@
"%(senderName)s set a profile picture": "%(senderName)s caktoi një foto profili",
"%(senderName)s changed their profile picture": "%(senderName)s ndryshoi foton e vet të profilit",
"%(senderName)s removed their profile picture": "%(senderName)s hoqi foton e vet të profilit",
- "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s hoqi emrin e vet në ekran (%(oldDisplayName)s).",
+ "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s hoqi emrin e vet në ekran (%(oldDisplayName)s)",
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s caktoi për veten emër ekrani %(displayName)s",
"%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s ndryshoi emrin e vet në ekran si %(displayName)s",
- "%(senderName)s banned %(targetName)s": "%(senderName)s dëboi %(targetName)s.",
+ "%(senderName)s banned %(targetName)s": "%(senderName)s dëboi %(targetName)s",
"%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s dëboi %(targetName)s: %(reason)s",
"%(targetName)s accepted an invitation": "%(targetName)s pranoi një ftesë",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s pranoi ftesën për %(displayName)s",
"Some invites couldn't be sent": "S’u dërguan dot disa nga ftesat",
- "We sent the others, but the below people couldn't be invited to ": "I dërguam të tjerat, por personat më poshtë s’u ftuan dot te "
+ "We sent the others, but the below people couldn't be invited to ": "I dërguam të tjerat, por personat më poshtë s’u ftuan dot te ",
+ "Unnamed audio": "Audio pa emër",
+ "Forward": "Përcille",
+ "Sent": "U dërgua",
+ "Error processing audio message": "Gabim në përpunim mesazhi audio",
+ "Show %(count)s other previews|one": "Shfaq %(count)s paraparje tjetër",
+ "Show %(count)s other previews|other": "Shfaq %(count)s paraparje të tjera",
+ "Images, GIFs and videos": "Figura, GIF-e dhe video",
+ "Code blocks": "Blloqe kodi",
+ "To view all keyboard shortcuts, click here.": "Që të shihni krejt shkurtoret e tastierës, klikoni këtu.",
+ "Keyboard shortcuts": "Shkurtore tastiere",
+ "Use Ctrl + F to search timeline": "Përdorni Ctrl + F që të kërkohet te rrjedha kohore",
+ "Use Command + F to search timeline": "Përdorni Command + F që të kërkohet te rrjedha kohore",
+ "User %(userId)s is already invited to the room": "Përdoruesi %(userId)s është ftuar tashmë te dhoma",
+ "Integration manager": "Përgjegjës integrimesh",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-i juaj nuk ju lejon të përdorni një përgjegjës integrimesh për të bërë këtë. Ju lutemi, lidhuni me përgjegjësin.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s & përgjegjësin tuaj të integrimeve.",
+ "Identity server is": "Shërbyes identitetesh është",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Përgjegjësit e integrimeve marrin të dhëna formësimi, dhe mund të ndryshojnë widget-e, të dërgojnë ftesa dhome, dhe të caktojnë shkallë pushteti në emër tuajin.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Përdorni një përgjegjës integrimesh që të administroni robotë, widget-e dhe paketa ngjitësish.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Përdorni një përgjegjës integrimesh (%(serverName)s) që të administroni robotë, widget-e dhe paketa ngjitësish.",
+ "Identity server": "Shërbyes identitetesh",
+ "Identity server (%(server)s)": "Shërbyes identitetesh (%(server)s)",
+ "Could not connect to identity server": "S’u lidh dot te shërbyes identitetesh",
+ "Not a valid identity server (status code %(code)s)": "Shërbyes identitetesh i pavlefshëm (kod gjendjeje %(code)s)",
+ "Identity server URL must be HTTPS": "URL-ja e shërbyesit të identiteteve duhet të jetë HTTPS",
+ "Unable to transfer call": "S’arrihet të shpërngulet thirrje",
+ "Unable to copy a link to the room to the clipboard.": "S’arrihet të kopjohet në të papastër një lidhje për te dhoma.",
+ "Unable to copy room link": "S’arrihet të kopjohet lidhja e dhomës",
+ "User Directory": "Drejtori Përdoruesi",
+ "Copy Link": "Kopjoji Lidhjen",
+ "Displaying time": "Kohë shfaqjeje",
+ "There was an error loading your notification settings.": "Pati një gabim në ngarkimin e rregullimeve tuaja për njoftimet.",
+ "Mentions & keywords": "Përmendje & fjalëkyçe",
+ "Global": "Global",
+ "New keyword": "Fjalëkyç i ri",
+ "Keyword": "Fjalëkyç",
+ "Enable email notifications for %(email)s": "Aktivizo njoftime me email për %(email)s",
+ "Enable for this account": "Aktivizoje për këtë llogari",
+ "An error occurred whilst saving your notification preferences.": "Ndodhi një gabim teksa ruheshin parapëlqimet tuaja për njoftimet.",
+ "Error saving notification preferences": "Gabim në ruajtje parapëlqimesh për njoftimet",
+ "Messages containing keywords": "Mesazhe që përmbajnë fjalëkyçe",
+ "Transfer Failed": "Shpërngulja Dështoi",
+ "Copy Room Link": "Kopjo Lidhje Dhome",
+ "Message bubbles": "Flluska mesazhesh",
+ "IRC": "IRC",
+ "New layout switcher (with message bubbles)": "Këmbyes i ri skemash (me flluska mesazhesh)",
+ "Connected": "E lidhur",
+ "Downloading": "Po shkarkohet",
+ "The call is in an unknown state!": "Thirrja gjendet në një gjendje të panjohur!",
+ "Call back": "Thirreni ju",
+ "You missed this call": "E humbët këtë thirrje",
+ "This call has failed": "Kjo thirrje ka dështuar",
+ "Unknown failure: %(reason)s)": "Dështim i panjohur: %(reason)s)",
+ "No answer": "S’ka përgjigje",
+ "An unknown error occurred": "Ndodhi një gabim i panjohur",
+ "Their device couldn't start the camera or microphone": "Pajisja e tyre s’nisi dot kamerën ose mikrofonin",
+ "Connection failed": "Lidhja dështoi",
+ "Could not connect media": "S’u lidh dot me median",
+ "This call has ended": "Kjo thirrje ka përfunduar",
+ "Error downloading audio": "Gabim në shkarkim audioje",
+ "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.": "Ju lutemi, kini parasysh se përmirësimi do të prodhojë një version të ri të dhomës . Krejt mesazhet e tanishëm do të mbeten në këtë dhomë të arkivuar.",
+ "Automatically invite members from this room to the new one": "Fto automatikisht anëtarë prej kësaj dhome te e reja",
+ "These are likely ones other room admins are a part of.": "Këto ka shumë mundësi të jetë ato ku përgjegjës të tjerë dhomash janë pjesë.",
+ "Other spaces or rooms you might not know": "Hapësira ose dhoma të tjera që mund të mos i dini",
+ "Spaces you know that contain this room": "Hapësira që e dini se përmbajnë këtë dhomë",
+ "Search spaces": "Kërkoni në hapësira",
+ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Vendosni se prej cilave hapësira mund të hyhet në këtë dhomë. Nëse përzgjidhet një hapësirë, anëtarët e saj do të mund ta gjejnë dhe hyjnë te .",
+ "Select spaces": "Përzgjidhni hapësira",
+ "Room visibility": "Dukshmëri dhome",
+ "Visible to space members": "I dukshëm për anëtarë të hapësirë",
+ "Public room": "Dhomë publike",
+ "Private room (invite only)": "Dhomë private (vetëm me ftesa)",
+ "Create a room": "Krijoni një dhomë",
+ "Only people invited will be able to find and join this room.": "Vetëm personat e ftuar do të jenë në gjendje ta gjejnë dhe hyjnë në këtë dhomë.",
+ "Anyone will be able to find and join this room, not just members of .": "Cilido do të jetë në gjendje të gjejë dhe hyjë në këtë dhomë, jo thjesht vetëm anëtarët e .",
+ "You can change this at any time from room settings.": "Këtë mund ta ndryshoni kurdo, që nga rregullimet e dhomës.",
+ "Everyone in will be able to find and join this room.": "Cilido te do të jetë në gjendje të gjejë dhe hyjë në këtë dhomë.",
+ "Image": "Figurë",
+ "Sticker": "Ngjitës",
+ "The voice message failed to upload.": "Dështoi ngarkimi i mesazhit zanor.",
+ "Access": "Hyrje",
+ "People with supported clients will be able to join the room without having a registered account.": "Persona me klientë të mbuluar do të jenë në gjendje të hyjnë te dhoma pa pasur ndonjë llogari të regjistruar.",
+ "Decide who can join %(roomName)s.": "Vendosni se cilët mund të hyjnë te %(roomName)s.",
+ "Space members": "Anëtarë hapësire",
+ "Anyone in a space can find and join. You can select multiple spaces.": "Mund të përzgjidhni një hapësirë që mund të gjejë dhe hyjë. Mund të përzgjidhni disa hapësira.",
+ "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Cilido te %(spaceName)s mund ta gjejë dhe hyjë. Mund të përzgjidhni edhe hapësira të tjera.",
+ "Spaces with access": "Hapësira me hyrje",
+ "Anyone in a space can find and join. Edit which spaces can access here. ": "Cilido në një hapësirë mund ta gjejë dhe hyjë. Përpunoni se cilat hapësira kanë hyrje këtu. ",
+ "Currently, %(count)s spaces have access|other": "Deri tani, %(count)s hapësira kanë hyrje",
+ "& %(count)s more|other": "& %(count)s më tepër",
+ "Upgrade required": "Lypset domosdo përmirësim",
+ "Anyone can find and join.": "Kushdo mund ta gjejë dhe hyjë në të.",
+ "Only invited people can join.": "Vetëm personat e ftuar mund të hyjnë.",
+ "Private (invite only)": "Private (vetëm me ftesa)",
+ "This upgrade will allow members of selected spaces access to this room without an invite.": "Ky përmirësim do t’u lejojë anëtarëve të hapësirave të përzgjedhura të hyjnë në këtë dhomë pa ndonjë ftesë.",
+ "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Kjo e bën të lehtë mbajtjen private të dhomave në një hapësirë, ndërkohë që u lejon njerëzve në hapësirë të gjejnë dhe hyjnë në të tilla. Krejt dhomat e reja në një hapësirë do ta ofrojnë këtë mundësi.",
+ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Që të ndihmoni anëtarë hapësirash të gjejnë dhe hyjnë në një dhomë private, kaloni te rregullimet e Sigurisë & Privatësisë së dhomës.",
+ "Help space members find private rooms": "Ndihmoni anëtarë hapësirash të gjejnë dhoma private",
+ "Help people in spaces to find and join private rooms": "Ndihmoni persona në hapësira të gjejnë dhe hyjnë në dhoma private",
+ "New in the Spaces beta": "E re në Hapësira beta"
}
diff --git a/src/i18n/strings/sr.json b/src/i18n/strings/sr.json
index 49f87321f7..5af8ffe820 100644
--- a/src/i18n/strings/sr.json
+++ b/src/i18n/strings/sr.json
@@ -1760,5 +1760,7 @@
"You're already in a call with this person.": "Већ разговарате са овом особом.",
"Already in call": "Већ у позиву",
"Whether you're using %(brand)s as an installed Progressive Web App": "Без обзира да ли користите %(brand)s као инсталирану Прогресивну веб апликацију",
- "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Без обзира да ли користите функцију „breadcrumbs“ (аватари изнад листе соба)"
+ "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Без обзира да ли користите функцију „breadcrumbs“ (аватари изнад листе соба)",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Коришћење овог виџета може да дели податке са %(widgetDomain)s и вашим интеграционим менаџером.",
+ "Identity server is": "Идентитетски сервер је"
}
diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index b36af42f5e..e5e9cc5d34 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -3356,5 +3356,16 @@
"We sent the others, but the below people couldn't be invited to ": "Vi skickade de andra, men personerna nedan kunde inte bjudas in till ",
"What this user is writing is wrong.\nThis will be reported to the room moderators.": "Vad användaren skriver är fel.\nDet här kommer att anmälas till rumsmoderatorerna.",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Prototyp av anmälan till moderatorer. I rum som söder moderering så kommer `anmäl`-knappen att låta dig anmäla olämpligt beteende till rummets moderatorer",
- "Report": "Rapportera"
+ "Report": "Rapportera",
+ "Integration manager": "Integrationshanterare",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Din %(brand)s tillåter dig inte att använda en integrationshanterare för att göra detta. Vänligen kontakta en administratör.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Att använda denna widget kan dela data med %(widgetDomain)s och din integrationshanterare.",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationshanterare får konfigurationsdata och kan ändra widgetar, skicka rumsinbjudningar och ställa in behörighetsnivåer å dina vägnar.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Använd en integrationshanterare för att hantera bottar, widgets och dekalpaket.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Använd en integrationshanterare (%(serverName)s) för att hantera bottar, widgets och dekalpaket.",
+ "Identity server": "Identitetsserver",
+ "Identity server (%(server)s)": "Identitetsserver (%(server)s)",
+ "Could not connect to identity server": "Kunde inte ansluta till identitetsservern",
+ "Not a valid identity server (status code %(code)s)": "Inte en giltig identitetsserver (statuskod %(code)s)",
+ "Identity server URL must be HTTPS": "URL för identitetsserver måste vara HTTPS"
}
diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json
index 0458d3226a..687273729b 100644
--- a/src/i18n/strings/tr.json
+++ b/src/i18n/strings/tr.json
@@ -101,7 +101,7 @@
"Failed to set display name": "Görünür ismi ayarlama başarısız oldu",
"Failed to unban": "Yasağı kaldırmak başarısız oldu",
"Failed to upload profile picture!": "Profil resmi yükleme başarısız oldu!",
- "Failed to verify email address: make sure you clicked the link in the email": "Eposta adresini doğrulamadı: epostadaki bağlantıya tıkladığınızdan emin olun",
+ "Failed to verify email address: make sure you clicked the link in the email": "E-posta adresi doğrulanamadı: E-postadaki bağlantıya tıkladığınızdan emin olun",
"Failure to create room": "Oda oluşturulamadı",
"Favourite": "Favori",
"Favourites": "Favoriler",
@@ -1695,7 +1695,7 @@
"Visibility in Room List": "Oda Listesindeki Görünürlük",
"Confirm adding email": "E-posta adresini eklemeyi onayla",
"Click the button below to confirm adding this email address.": "E-posta adresini eklemeyi kabul etmek için aşağıdaki tuşa tıklayın.",
- "Confirm adding phone number": "Telefon numayasını ekleyi onayla",
+ "Confirm adding phone number": "Telefon numarası eklemeyi onayla",
"Click the button below to confirm adding this phone number.": "Telefon numarasını eklemeyi kabul etmek için aşağıdaki tuşa tıklayın.",
"Are you sure you want to cancel entering passphrase?": "Parola girmeyi iptal etmek istediğinizden emin misiniz?",
"Room name or address": "Oda adı ya da adresi",
@@ -2544,5 +2544,47 @@
"We couldn't log you in": "Sizin girişinizi yapamadık",
"You're already in a call with this person.": "Bu kişi ile halihazırda çağrıdasınız.",
"The user you called is busy.": "Aradığınız kullanıcı meşgul.",
- "User Busy": "Kullanıcı Meşgul"
+ "User Busy": "Kullanıcı Meşgul",
+ "Got it": "Anlaşıldı",
+ "Verified": "Doğrulanmış",
+ "You've successfully verified %(displayName)s!": "%(displayName)s başarıyla doğruladınız!",
+ "You've successfully verified %(deviceName)s (%(deviceId)s)!": "%(deviceName)s (%(deviceId)s) başarıyla doğruladınız!",
+ "You've successfully verified your device!": "Cihazınızı başarıyla doğruladınız!",
+ "Edit devices": "Cihazları düzenle",
+ "Delete recording": "Kaydı sil",
+ "Stop the recording": "Kaydı durdur",
+ "We didn't find a microphone on your device. Please check your settings and try again.": "Cihazınızda bir mikrofon bulamadık. Lütfen ayarlarınızı kontrol edin ve tekrar deneyin.",
+ "No microphone found": "Mikrofon bulunamadı",
+ "Empty room": "Boş oda",
+ "Suggested Rooms": "Önerilen Odalar",
+ "View message": "Mesajı görüntüle",
+ "Invite to just this room": "Sadece bu odaya davet et",
+ "%(seconds)ss left": "%(seconds)s saniye kaldı",
+ "Send message": "Mesajı gönder",
+ "Your message was sent": "Mesajınız gönderildi",
+ "Encrypting your message...": "Mesajınız şifreleniyor...",
+ "Sending your message...": "Mesajınız gönderiliyor...",
+ "Code blocks": "Kod blokları",
+ "Displaying time": "Zamanı görüntüle",
+ "To view all keyboard shortcuts, click here.": "Tüm klavye kısayollarını görmek için buraya tıklayın.",
+ "Keyboard shortcuts": "Klavye kısayolları",
+ "Visibility": "Görünürlük",
+ "Save Changes": "Değişiklikleri Kaydet",
+ "Saving...": "Kaydediliyor...",
+ "Invite with email or username": "E-posta veya kullanıcı adı ile davet et",
+ "Invite people": "İnsanları davet et",
+ "Share invite link": "Davet bağlantısını paylaş",
+ "Click to copy": "Kopyalamak için tıklayın",
+ "You can change these anytime.": "Bunları istediğiniz zaman değiştirebilirsiniz.",
+ "You can change this later": "Bunu daha sonra değiştirebilirsiniz",
+ "Change which room, message, or user you're viewing": "Görüntülediğiniz odayı, mesajı veya kullanıcıyı değiştirin",
+ "%(targetName)s accepted an invitation": "%(targetName)s daveti kabul etti",
+ "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s, %(displayName)s kişisinin davetini kabul etti",
+ "Integration manager": "Bütünleştirme Yöneticisi",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Botları, görsel bileşenleri ve çıkartma paketlerini yönetmek için bir entegrasyon yöneticisi kullanın.",
+ "Identity server": "Kimlik sunucusu",
+ "Identity server (%(server)s)": "(%(server)s) Kimlik Sunucusu",
+ "Could not connect to identity server": "Kimlik Sunucusuna bağlanılamadı",
+ "Not a valid identity server (status code %(code)s)": "Geçerli bir Kimlik Sunucu değil ( durum kodu %(code)s )",
+ "Identity server URL must be HTTPS": "Kimlik Sunucu URL adresi HTTPS olmak zorunda"
}
diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 92da704837..753f748850 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -506,7 +506,7 @@
"Upload Error": "Помилка відвантаження",
"Failed to upload image": "Не вдалось відвантажити зображення",
"Upload avatar": "Завантажити аватар",
- "For security, this session has been signed out. Please sign in again.": "З метою безпеки вашу сесію було завершено. Зайдіть, будь ласка, знову.",
+ "For security, this session has been signed out. Please sign in again.": "З метою безпеки ваш сеанс було завершено. Увійдіть знову.",
"Upload an avatar:": "Завантажити аватар:",
"Custom (%(level)s)": "Власний (%(level)s)",
"Error upgrading room": "Помилка оновлення кімнати",
@@ -541,7 +541,7 @@
"Cancel entering passphrase?": "Скасувати введення парольної фрази?",
"Enter passphrase": "Введіть парольну фразу",
"Setting up keys": "Налаштовування ключів",
- "Verify this session": "Звірити цю сесію",
+ "Verify this session": "Звірити цей сеанс",
"Sign In or Create Account": "Увійти або створити обліковий запис",
"Use your account or create a new one to continue.": "Скористайтесь вашим обліковим записом або створіть нову, щоб продовжити.",
"Create Account": "Створити обліковий запис",
@@ -564,10 +564,10 @@
"For help with using %(brand)s, click here .": "Якщо необхідна допомога у користуванні %(brand)s'ом, клацніть тут .",
"For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Якщо необхідна допомога у користуванні %(brand)s'ом, клацніть тут або розпочніть балачку з нашим ботом, клацнувши на кнопці нижче.",
"Join the conversation with an account": "Приєднатись до бесіди з обліковим записом",
- "Unable to restore session": "Неможливо відновити сесію",
- "We encountered an error trying to restore your previous session.": "Ми натрапили на помилку, намагаючись відновити вашу попередню сесію.",
+ "Unable to restore session": "Не вдалося відновити сеанс",
+ "We encountered an error trying to restore your previous session.": "Ми натрапили на помилку, намагаючись відновити ваш попередній сеанс.",
"Please install Chrome , Firefox , or Safari for the best experience.": "Для найкращих вражень від користування встановіть, будь ласка, Chrome , Firefox , або Safari .",
- "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Ваш обліковий запис має перехресно-підписувану ідентичність у таємному сховищі, але вона ще не є довіреною у цій сесії.",
+ "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Ваш обліковий запис має перехресно-підписувану ідентичність у таємному сховищі, але воно ще не є довіреним у цьому сеансі.",
"in account data": "у даних облікового запису",
"Clear notifications": "Очистити сповіщення",
"Add an email address to configure email notifications": "Додати адресу е-пошти для налаштування поштових сповіщень",
@@ -585,8 +585,8 @@
"Confirm account deactivation": "Підтвердьте знедіювання облікового запису",
"To continue, please enter your password:": "Щоб продовжити, введіть, будь ласка, ваш пароль:",
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible. ": "Ваш обліковий запис стане назавжди невикористовним. Ви не матимете змоги увійти в нього і ніхто не зможе перереєструватись під цим користувацьким ID. Це призведе до виходу вашого облікового запису з усіх кімнат та до видалення деталей вашого облікового запису з вашого серверу ідентифікації. Ця дія є безповоротною. ",
- "Verify session": "Звірити сесію",
- "Session name": "Назва сесії",
+ "Verify session": "Звірити сеанс",
+ "Session name": "Назва сеансу",
"Session ID": "ID сеансу",
"Session key": "Ключ сеансу",
"%(count)s of your messages have not been sent.|one": "Ваше повідомлення не було надіслано.",
@@ -697,7 +697,7 @@
"You signed in to a new session without verifying it:": "Ви увійшли в новий сеанс, не підтвердивши його:",
"Verify your other session using one of the options below.": "Перевірте інший сеанс за допомогою одного із варіантів знизу.",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) починає новий сеанс без його підтвердження:",
- "Ask this user to verify their session, or manually verify it below.": "Попросіть цього користувача підтвердити сесію, або підтвердіть її власноруч нижче.",
+ "Ask this user to verify their session, or manually verify it below.": "Попросіть цього користувача підтвердити сеанс, або підтвердьте його власноруч унизу.",
"Not Trusted": "Недовірене",
"Manually Verify by Text": "Ручна перевірка за допомогою тексту",
"Interactively verify by Emoji": "Інтерактивно звірити за допомогою емодзі",
@@ -973,10 +973,10 @@
"not found": "не знайдено",
"Cross-signing private keys:": "Приватні ключі для кросс-підпису:",
"exists": "існує",
- "Delete sessions|other": "Видалити сесії",
- "Delete sessions|one": "Видалити сесію",
- "Delete %(count)s sessions|other": "Видалити %(count)s сесій",
- "Delete %(count)s sessions|one": "Видалити %(count)s сесій",
+ "Delete sessions|other": "Видалити сеанси",
+ "Delete sessions|one": "Видалити сеанс",
+ "Delete %(count)s sessions|other": "Видалити %(count)s сеансів",
+ "Delete %(count)s sessions|one": "Видалити %(count)s сеансів",
"ID": "ID",
"Public Name": "Публічне ім'я",
" to store messages from ": " зберігання повідомлень від ",
@@ -1630,5 +1630,16 @@
"Send text messages as you in this room": "Надіслати текстові повідомлення у цю кімнату від свого імені",
"Send messages as you in your active room": "Надіслати повідомлення у свою активну кімнату від свого імені",
"Send messages as you in this room": "Надіслати повідомлення у цю кімнату від свого імені",
- "Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим"
+ "Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим",
+ "Integration manager": "Менеджер інтеграцій",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Ваш %(brand)s не дозволяє вам користуватись для цього менеджером інтеграцій. Зверніться до адміністратора.",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Користування цим знадобом може призвести до поширення ваших даних з %(widgetDomain)s та вашим менеджером інтеграцій.",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджери інтеграцій отримують дані конфігурації та можуть змінювати знадоби, надсилати запрошення у кімнати й встановлювати рівні повноважень від вашого імені.",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій для керування ботами, знадобами та паками наліпок.",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій %(serverName)s для керування ботами, знадобами та паками наліпок.",
+ "Identity server": "Сервер ідентифікації",
+ "Identity server (%(server)s)": "Сервер ідентифікації (%(server)s)",
+ "Could not connect to identity server": "Не вдалося під'єднатись до сервера ідентифікації",
+ "There was an error looking up the phone number": "Сталася помилка під час пошуку номеру телефону",
+ "Unable to look up phone number": "Неможливо знайти номер телефону"
}
diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json
index aec8580ef1..9bcb16c061 100644
--- a/src/i18n/strings/vi.json
+++ b/src/i18n/strings/vi.json
@@ -342,5 +342,19 @@
"Confirm adding email": "Xác nhận việc thêm email",
"Add Phone Number": "Thêm Số Điện Thoại",
"Click the button below to confirm adding this phone number.": "Nhấn vào nút dưới đây để xác nhận việc thêm số điện thoại này.",
- "Confirm": "Xác nhận"
+ "Confirm": "Xác nhận",
+ "No other application is using the webcam": "Không có ứng dụng nào khác đang sử dụng webcam",
+ "Permission is granted to use the webcam": "Quyền được cấp để sử dụng webcam",
+ "A microphone and webcam are plugged in and set up correctly": "Micro và webcam đã được cắm và thiết lập đúng cách",
+ "Call failed because webcam or microphone could not be accessed. Check that:": "Cuộc gọi không thành công vì không thể truy cập webcam hoặc micrô. Kiểm tra xem:",
+ "Unable to access webcam / microphone": "Không thể truy cập webcam / micro",
+ "The call could not be established": "Không thể thiết lập cuộc gọi",
+ "The user you called is busy.": "Người dùng mà bạn gọi đang bận",
+ "User Busy": "Người dùng đang bận",
+ "The other party declined the call.": "Bên kia đã từ chối cuộc gọi.",
+ "Call Declined": "Cuộc gọi bị từ chối",
+ "Your user agent": "Hành động của bạn",
+ "Single Sign On": "Single Sign On",
+ "Confirm adding this email address by using Single Sign On to prove your identity.": "Xác nhận việc thêm địa chỉ email này bằng cách sử dụng Single Sign On để chứng minh danh tính của bạn.",
+ "Use Single Sign On to continue": "Sử dụng Signle Sign On để tiếp tục"
}
diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json
index 75ab903ebe..24129dc6c3 100644
--- a/src/i18n/strings/vls.json
+++ b/src/i18n/strings/vls.json
@@ -1445,5 +1445,11 @@
"Remove %(email)s?": "%(email)s verwydern?",
"Remove %(phone)s?": "%(phone)s verwydern?",
"Explore rooms": "Gesprekkn ountdekkn",
- "Create Account": "Account anmoakn"
+ "Create Account": "Account anmoakn",
+ "Integration manager": "Integroasjebeheerder",
+ "Identity server": "Identiteitsserver",
+ "Identity server (%(server)s)": "Identiteitsserver (%(server)s)",
+ "Could not connect to identity server": "Kostege geen verbindienge moakn me den identiteitsserver",
+ "Not a valid identity server (status code %(code)s)": "Geen geldigen identiteitsserver (statuscode %(code)s)",
+ "Identity server URL must be HTTPS": "Den identiteitsserver-URL moet HTTPS zyn"
}
diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index 88ebb8f4cf..2472ac479e 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -3383,5 +3383,17 @@
"%(targetName)s accepted an invitation": "%(targetName)s 已接受邀请",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s 已接受 %(displayName)s 的邀请",
"Some invites couldn't be sent": "部分邀请无法送达",
- "We sent the others, but the below people couldn't be invited to ": "我们已向其他人发送邀请,除了以下无法邀请至 的人"
+ "We sent the others, but the below people couldn't be invited to ": "我们已向其他人发送邀请,除了以下无法邀请至 的人",
+ "Integration manager": "集成管理器",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "你的 %(brand)s 不允许你使用集成管理器来完成此操作。请联系管理员。",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "使用此挂件可能会和 %(widgetDomain)s 及您的集成管理器共享数据 。",
+ "Identity server is": "身份认证服务器是",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以你的名义修改挂件、发送聊天室邀请及设置权限级别。",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴纸包。",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴纸包。",
+ "Identity server": "身份服务器",
+ "Identity server (%(server)s)": "身份服务器(%(server)s)",
+ "Could not connect to identity server": "无法连接到身份服务器",
+ "Not a valid identity server (status code %(code)s)": "不是有效的身份服务器(状态码 %(code)s)",
+ "Identity server URL must be HTTPS": "身份服务器连接必须是 HTTPS"
}
diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 03cebcb083..74234746f6 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -3484,5 +3484,104 @@
"%(targetName)s accepted an invitation": "%(targetName)s 接受了邀請",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s 已接受 %(displayName)s 的邀請",
"Some invites couldn't be sent": "部份邀請無法傳送",
- "We sent the others, but the below people couldn't be invited to ": "我們已將邀請傳送給其他人,但以下的人無法邀請至 "
+ "We sent the others, but the below people couldn't be invited to ": "我們已將邀請傳送給其他人,但以下的人無法邀請至 ",
+ "Unnamed audio": "未命名的音訊",
+ "Error processing audio message": "處理音訊訊息時出現問題",
+ "Show %(count)s other previews|one": "顯示 %(count)s 個其他預覽",
+ "Show %(count)s other previews|other": "顯示 %(count)s 個其他預覽",
+ "Images, GIFs and videos": "圖片、GIF 與影片",
+ "Code blocks": "程式碼區塊",
+ "Displaying time": "顯示時間",
+ "To view all keyboard shortcuts, click here.": "要檢視所有鍵盤快捷鍵,請點擊此處。",
+ "Keyboard shortcuts": "鍵盤快捷鍵",
+ "Use Ctrl + F to search timeline": "使用 Ctrl + F 來搜尋時間軸",
+ "Use Command + F to search timeline": "使用 Command + F 來搜尋時間軸",
+ "User %(userId)s is already invited to the room": "使用者 %(userId)s 已被邀請至聊天室",
+ "Integration manager": "整合管理員",
+ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "您的 %(brand)s 不允許您使用整合管理員來執行此動作。請聯絡管理員。",
+ "Using this widget may share data with %(widgetDomain)s & your integration manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 。",
+ "Identity server is": "身分認證伺服器是",
+ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "整合管理員接收設定資料,並可以代表您可以修改小工具、傳送聊天室邀請並設定權限等級。",
+ "Use an integration manager to manage bots, widgets, and sticker packs.": "使用整合管理員以管理機器人、小工具與貼紙包。",
+ "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用整合管理員 (%(serverName)s) 以管理機器人、小工具與貼紙包。",
+ "Identity server": "身份識別伺服器",
+ "Identity server (%(server)s)": "身份識別伺服器 (%(server)s)",
+ "Could not connect to identity server": "無法連線至身份識別伺服器",
+ "Not a valid identity server (status code %(code)s)": "不是有效的身份識別伺服器(狀態碼 %(code)s)",
+ "Identity server URL must be HTTPS": "身份識別伺服器 URL 必須為 HTTPS",
+ "Unable to copy a link to the room to the clipboard.": "無法複製聊天室連結至剪貼簿。",
+ "Unable to copy room link": "無法複製聊天室連結",
+ "User Directory": "使用者目錄",
+ "Copy Link": "複製連結",
+ "There was an error loading your notification settings.": "載入您的通知設定時發生錯誤。",
+ "Mentions & keywords": "提及與關鍵字",
+ "Global": "全域",
+ "New keyword": "新關鍵字",
+ "Keyword": "關鍵字",
+ "Enable email notifications for %(email)s": "為 %(email)s 啟用電子郵件通知",
+ "Enable for this account": "為此帳號啟用",
+ "An error occurred whilst saving your notification preferences.": "儲存您的通知偏好設定時遇到錯誤。",
+ "Error saving notification preferences": "儲存通知偏好設定時發生問題",
+ "Messages containing keywords": "包含關鍵字的訊息",
+ "Transfer Failed": "轉接失敗",
+ "Unable to transfer call": "無法轉接通話",
+ "Copy Room Link": "複製聊天室連結",
+ "Downloading": "正在下載",
+ "The call is in an unknown state!": "通話處於未知狀態!",
+ "Call back": "回撥",
+ "You missed this call": "您錯過了此通話",
+ "This call has failed": "此通話失敗",
+ "Unknown failure: %(reason)s)": "未知的錯誤:%(reason)s",
+ "No answer": "無回應",
+ "An unknown error occurred": "出現未知錯誤",
+ "Their device couldn't start the camera or microphone": "他們的裝置無法啟動攝影機或麥克風",
+ "Connection failed": "連線失敗",
+ "Could not connect media": "無法連結媒體",
+ "This call has ended": "此通話已結束",
+ "Connected": "已連線",
+ "Message bubbles": "訊息泡泡",
+ "IRC": "IRC",
+ "New layout switcher (with message bubbles)": "新的佈局切換器(帶有訊息泡泡)",
+ "Image": "圖片",
+ "Sticker": "貼紙",
+ "Error downloading audio": "下載音訊時發生錯誤",
+ "Anyone in a space can find and join. Edit which spaces can access here. ": "任何在空間中的人都可以找到並加入。編輯哪些空間可以存取這個地方。 ",
+ "Please note upgrading will make a new version of the room . All current messages will stay in this archived room.": "請注意,升級會讓聊天是變成全新的版本 。目前所有的訊息都只會留在被封存的聊天室。",
+ "Automatically invite members from this room to the new one": "自動將該聊天室的成員邀請至新的聊天室",
+ "These are likely ones other room admins are a part of.": "這些可能是其他聊天室管理員的一部分。",
+ "Other spaces or rooms you might not know": "您可能不知道的其他空間或聊天室",
+ "Spaces you know that contain this room": "您知道的包含此聊天是的空間",
+ "Search spaces": "搜尋空間",
+ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "決定哪些空間可以存取此聊天室。若選取了空間,其成員就可以找到並加入 。",
+ "Select spaces": "選取空間",
+ "You're removing all spaces. Access will default to invite only": "您正在移除所有空間。存取權限將會預設為僅邀請",
+ "Room visibility": "聊天室能見度",
+ "Visible to space members": "對空間成員可見",
+ "Public room": "公開聊天室",
+ "Private room (invite only)": "私人聊天室(僅邀請)",
+ "Create a room": "建立聊天室",
+ "Only people invited will be able to find and join this room.": "僅被邀請的夥伴才能找到並加入此聊天室。",
+ "Anyone will be able to find and join this room, not just members of .": "任何人都將可以找到並加入此聊天室,而不只是 的成員。",
+ "You can change this at any time from room settings.": "您隨時都可以從聊天室設定變更此設定。",
+ "Everyone in will be able to find and join this room.": "每個在 中的人都將可以找到並加入此聊天室。",
+ "The voice message failed to upload.": "語音訊息上傳失敗。",
+ "Access": "存取",
+ "People with supported clients will be able to join the room without having a registered account.": "有受支援的客戶端的夥伴不需要註冊帳號就可以加入聊天室。",
+ "Decide who can join %(roomName)s.": "決定誰可以加入 %(roomName)s。",
+ "Space members": "空間成員",
+ "Anyone in a space can find and join. You can select multiple spaces.": "空間中的任何人都可以找到並加入。您可以選取多個空間。",
+ "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "任何在 %(spaceName)s 的人都可以找到並加入。您也可以選取其他空間。",
+ "Spaces with access": "可存取的空間",
+ "Currently, %(count)s spaces have access|other": "目前,%(count)s 個空間可存取",
+ "& %(count)s more|other": "以及 %(count)s 個",
+ "Upgrade required": "必須升級",
+ "Anyone can find and join.": "任何人都可以找到並加入。",
+ "Only invited people can join.": "僅被邀請的夥伴可以加入。",
+ "Private (invite only)": "私人(僅邀請)",
+ "This upgrade will allow members of selected spaces access to this room without an invite.": "此升級讓選定空間的成員不需要邀請就可以存取此聊天室。",
+ "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "這同時可以讓聊天室對空間保持隱密,又讓空間中的夥伴可以找到並加入這些聊天室。空間中的所有新聊天室都有此選項。",
+ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "要協助空間成員尋找並加入私人聊天室,請到該聊天室的「安全與隱私」設定。",
+ "Help space members find private rooms": "協助空間成員尋找私人聊天室",
+ "Help people in spaces to find and join private rooms": "協助空間中的夥伴尋找並加入私人聊天室",
+ "New in the Spaces beta": "Spaces 測試版的新功能"
}
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index e7329e4f2e..8b1d83b337 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -160,6 +160,17 @@ export function _t(text: string, variables?: IVariables, tags?: Tags): Translate
}
}
+/**
+ * Sanitizes unsafe text for the sanitizer, ensuring references to variables will not be considered
+ * replaceable by the translation functions.
+ * @param {string} text The text to sanitize.
+ * @returns {string} The sanitized text.
+ */
+export function sanitizeForTranslation(text: string): string {
+ // Add a non-breaking space so the regex doesn't trigger when translating.
+ return text.replace(/%\(([^)]*)\)/g, '%\xa0($1)');
+}
+
/*
* Similar to _t(), except only does substitutions, and no translation
* @param {string} text The text, e.g "click here now to %(foo)s".
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index f0bdb2e0e5..c36e2b90bf 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -124,6 +124,7 @@ export interface ISetting {
// not use this for new settings.
invertedSettingName?: string;
+ // XXX: Keep this around for re-use in future Betas
betaInfo?: {
title: string; // _td
caption: string; // _td
@@ -179,45 +180,14 @@ export const SETTINGS: {[setting: string]: ISetting} = {
feedbackSubheading: _td("Your feedback will help make spaces better. " +
"The more detail you can go into, the better."),
feedbackLabel: "spaces-feedback",
- extraSettings: [
- "feature_spaces.all_rooms",
- "feature_spaces.space_member_dms",
- "feature_spaces.space_dm_badges",
- ],
},
},
- "feature_spaces.all_rooms": {
- displayName: _td("Show all rooms in Home"),
- supportedLevels: LEVELS_FEATURE,
- default: true,
- controller: new ReloadOnChangeController(),
- },
- "feature_spaces.space_member_dms": {
- displayName: _td("Show people in spaces"),
- description: _td("If disabled, you can still add Direct Messages to Personal Spaces. " +
- "If enabled, you'll automatically see everyone who is a member of the Space."),
- supportedLevels: LEVELS_FEATURE,
- default: true,
- controller: new ReloadOnChangeController(),
- },
- "feature_spaces.space_dm_badges": {
- displayName: _td("Show notification badges for People in Spaces"),
- supportedLevels: LEVELS_FEATURE,
- default: false,
- controller: new ReloadOnChangeController(),
- },
"feature_dnd": {
isFeature: true,
displayName: _td("Show options to enable 'Do not disturb' mode"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
- "feature_voice_messages": {
- isFeature: true,
- displayName: _td("Send and receive voice messages"),
- supportedLevels: LEVELS_FEATURE,
- default: false,
- },
"feature_latex_maths": {
isFeature: true,
displayName: _td("Render LaTeX maths in messages"),
@@ -773,6 +743,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: null,
},
+ "Spaces.allRoomsInHome": {
+ displayName: _td("Show all rooms in Home"),
+ description: _td("All rooms you're in will appear in Home."),
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: false,
+ },
[UIFeature.RoomHistorySettings]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index 44f3d5d838..c5b83cbcd0 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -29,6 +29,8 @@ import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager";
import { SettingLevel } from "./SettingLevel";
import SettingsHandler from "./handlers/SettingsHandler";
+import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
+import { Action } from "../dispatcher/actions";
const defaultWatchManager = new WatchManager();
@@ -147,7 +149,7 @@ export default class SettingsStore {
* if the change in value is worthwhile enough to react upon.
* @returns {string} A reference to the watcher that was employed.
*/
- public static watchSetting(settingName: string, roomId: string, callbackFn: CallbackFn): string {
+ public static watchSetting(settingName: string, roomId: string | null, callbackFn: CallbackFn): string {
const setting = SETTINGS[settingName];
const originalSettingName = settingName;
if (!setting) throw new Error(`${settingName} is not a setting`);
@@ -193,7 +195,7 @@ export default class SettingsStore {
* @param {string} settingName The setting name to monitor.
* @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
*/
- public static monitorSetting(settingName: string, roomId: string) {
+ public static monitorSetting(settingName: string, roomId: string | null) {
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map());
@@ -201,8 +203,8 @@ export default class SettingsStore {
const registerWatcher = () => {
this.monitors.get(settingName).set(roomId, SettingsStore.watchSetting(
settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => {
- dis.dispatch({
- action: 'setting_updated',
+ dis.dispatch({
+ action: Action.SettingUpdated,
settingName,
roomId: inRoomId,
level,
diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts
index aceaf8b898..8a85ca354f 100644
--- a/src/stores/BreadcrumbsStore.ts
+++ b/src/stores/BreadcrumbsStore.ts
@@ -23,6 +23,8 @@ import { arrayHasDiff } from "../utils/arrays";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { SettingLevel } from "../settings/SettingLevel";
import SpaceStore from "./SpaceStore";
+import { Action } from "../dispatcher/actions";
+import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
const MAX_ROOMS = 20; // arbitrary
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
@@ -63,10 +65,11 @@ export class BreadcrumbsStore extends AsyncStoreWithClient {
protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return;
- if (payload.action === 'setting_updated') {
- if (payload.settingName === 'breadcrumb_rooms') {
+ if (payload.action === Action.SettingUpdated) {
+ const settingUpdatedPayload = payload as SettingUpdatedPayload;
+ if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') {
await this.updateRooms();
- } else if (payload.settingName === 'breadcrumbs') {
+ } else if (settingUpdatedPayload.settingName === 'breadcrumbs') {
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
}
} else if (payload.action === 'view_room') {
diff --git a/src/stores/LifecycleStore.ts b/src/stores/LifecycleStore.ts
index 7db50af7a1..97d3734a52 100644
--- a/src/stores/LifecycleStore.ts
+++ b/src/stores/LifecycleStore.ts
@@ -56,7 +56,7 @@ class LifecycleStore extends Store {
deferredAction: null,
});
break;
- case 'syncstate': {
+ case 'sync_state': {
if (payload.state !== 'PREPARED') {
break;
}
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index 65201134bf..da18646d0f 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -14,11 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import React from "react";
import { ListIteratee, Many, sortBy, throttle } from "lodash";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ISpaceSummaryRoom } from "matrix-js-sdk/src/@types/spaces";
+import { JoinRule } from "matrix-js-sdk/src/@types/partials";
+import { IRoomCapability } from "matrix-js-sdk/src/client";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
@@ -34,11 +37,17 @@ import { EnhancedMap, mapDiff } from "../utils/maps";
import { setHasDiff } from "../utils/sets";
import RoomViewStore from "./RoomViewStore";
import { Action } from "../dispatcher/actions";
-import { arrayHasDiff } from "../utils/arrays";
+import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays";
import { objectDiff } from "../utils/objects";
-import { arrayHasOrderChange } from "../utils/arrays";
import { reorderLexicographically } from "../utils/stringOrderField";
import { TAG_ORDER } from "../components/views/rooms/RoomList";
+import { shouldShowSpaceSettings } from "../utils/space";
+import ToastStore from "./ToastStore";
+import { _t } from "../languageHandler";
+import GenericToast from "../components/views/toasts/GenericToast";
+import Modal from "../Modal";
+import InfoDialog from "../components/views/dialogs/InfoDialog";
+import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
type SpaceKey = string | symbol;
@@ -52,6 +61,7 @@ export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
+export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour");
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
export interface ISuggestedRoom extends ISpaceSummaryRoom {
@@ -60,14 +70,10 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom {
const MAX_SUGGESTED_ROOMS = 20;
-// All of these settings cause the page to reload and can be costly if read frequently, so read them here only
+// This setting causes the page to reload and can be costly if read frequently, so read it here only
const spacesEnabled = SettingsStore.getValue("feature_spaces");
-const spacesTweakAllRoomsEnabled = SettingsStore.getValue("feature_spaces.all_rooms");
-const spacesTweakSpaceMemberDMsEnabled = SettingsStore.getValue("feature_spaces.space_member_dms");
-const spacesTweakSpaceDMBadgesEnabled = SettingsStore.getValue("feature_spaces.space_dm_badges");
-const homeSpaceKey = spacesTweakAllRoomsEnabled ? "ALL_ROOMS" : "HOME_SPACE";
-const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`;
+const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => {
@@ -95,10 +101,6 @@ const getRoomFn: FetchRoomFn = (room: Room) => {
};
export class SpaceStoreClass extends AsyncStoreWithClient {
- constructor() {
- super(defaultDispatcher, {});
- }
-
// The spaces representing the roots of the various tree-like hierarchies
private rootSpaces: Room[] = [];
// The list of rooms not present in any currently joined spaces
@@ -114,6 +116,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set();
private spaceOrderLocalEchoMap = new Map();
+ private _restrictedJoinRuleSupport?: IRoomCapability;
+ private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome");
+
+ constructor() {
+ super(defaultDispatcher, {});
+
+ SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
+ }
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
@@ -131,13 +141,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
return this._suggestedRooms;
}
- public async setActiveRoomInSpace(space: Room | null) {
+ public get allRoomsInHome(): boolean {
+ return this._allRoomsInHome;
+ }
+
+ public async setActiveRoomInSpace(space: Room | null): Promise {
if (space && !space.isSpaceRoom()) return;
if (space !== this.activeSpace) await this.setActiveSpace(space);
if (space) {
- const notificationState = this.getNotificationState(space.roomId);
- const roomId = notificationState.getFirstRoomWithNotifications();
+ const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications();
defaultDispatcher.dispatch({
action: "view_room",
room_id: roomId,
@@ -166,6 +179,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
}
+ public get restrictedJoinRuleSupport(): IRoomCapability {
+ return this._restrictedJoinRuleSupport;
+ }
+
/**
* Sets the active space, updates room list filters,
* optionally switches the user's room back to where they were when they last viewed that space.
@@ -188,7 +205,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// else if the last viewed room in this space is joined then view that
// else view space home or home depending on what is being clicked on
if (space?.getMyMembership() !== "invite" &&
- this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join"
+ this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" &&
+ this.getSpaceFilteredRoomIds(space).has(roomId)
) {
defaultDispatcher.dispatch({
action: "view_room",
@@ -215,6 +233,65 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
}
+ // New in Spaces beta toast for Restricted Join Rule
+ const lsKey = "mx_SpaceBeta_restrictedJoinRuleToastSeen";
+ if (contextSwitch && space?.getJoinRule() === JoinRule.Invite && shouldShowSpaceSettings(space) &&
+ space.getJoinedMemberCount() > 1 && !localStorage.getItem(lsKey)
+ && this.restrictedJoinRuleSupport?.preferred
+ ) {
+ const toastKey = "restrictedjoinrule";
+ ToastStore.sharedInstance().addOrReplaceToast({
+ key: toastKey,
+ title: _t("New in the Spaces beta"),
+ props: {
+ description: _t("Help people in spaces to find and join private rooms"),
+ acceptLabel: _t("Learn more"),
+ onAccept: () => {
+ localStorage.setItem(lsKey, "true");
+ ToastStore.sharedInstance().dismissToast(toastKey);
+
+ Modal.createTrackedDialog("New in the Spaces beta", "restricted join rule", InfoDialog, {
+ title: _t("Help space members find private rooms"),
+ description: <>
+ { _t("To help space members find and join a private room, " +
+ "go to that room's Security & Privacy settings.") }
+
+ { /* Reuses classes from TabbedView for simplicity, non-interactive */ }
+
+
+
+ { _t("General") }
+
+
+
+ { _t("Security & Privacy") }
+
+
+
+ { _t("Roles & Permissions") }
+
+
+
+ { _t("This makes it easy for rooms to stay private to a space, " +
+ "while letting people in the space find and join them. " +
+ "All new rooms in a space will have this option available.") }
+ >,
+ button: _t("OK"),
+ hasCloseButton: false,
+ fixedWidth: true,
+ });
+ },
+ rejectLabel: _t("Skip"),
+ onReject: () => {
+ localStorage.setItem(lsKey, "true");
+ ToastStore.sharedInstance().dismissToast(toastKey);
+ },
+ },
+ component: GenericToast,
+ priority: 35,
+ });
+ }
+
if (space) {
const suggestedRooms = await this.fetchSuggestedRooms(space);
if (this._activeSpace === space) {
@@ -258,7 +335,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}, roomId);
}
- private getChildren(spaceId: string): Room[] {
+ public getChildren(spaceId: string): Room[] {
const room = this.matrixClient?.getRoom(spaceId);
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
return sortBy(childEvents, ev => {
@@ -301,8 +378,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
return sortBy(parents, r => r.roomId)?.[0] || null;
}
+ public getKnownParents(roomId: string): Set {
+ return this.parentMap.get(roomId) || new Set();
+ }
+
public getSpaceFilteredRoomIds = (space: Room | null): Set => {
- if (!space && spacesTweakAllRoomsEnabled) {
+ if (!space && this.allRoomsInHome) {
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
}
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
@@ -399,7 +480,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
};
private showInHomeSpace = (room: Room) => {
- if (spacesTweakAllRoomsEnabled) return true;
+ if (this.allRoomsInHome) return true;
if (room.isSpaceRoom()) return false;
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
@@ -431,7 +512,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map();
- if (!spacesTweakAllRoomsEnabled) {
+ if (!this.allRoomsInHome) {
// put all room invites in the Home Space
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId)));
@@ -458,15 +539,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const roomIds = new Set(childRooms.map(r => r.roomId));
const space = this.matrixClient?.getRoom(spaceId);
- if (spacesTweakSpaceMemberDMsEnabled) {
- // Add relevant DMs
- space?.getMembers().forEach(member => {
- if (member.membership !== "join" && member.membership !== "invite") return;
- DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
- roomIds.add(roomId);
- });
+ // Add relevant DMs
+ space?.getMembers().forEach(member => {
+ if (member.membership !== "join" && member.membership !== "invite") return;
+ DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
+ roomIds.add(roomId);
});
- }
+ });
const newPath = new Set(parentPath).add(spaceId);
childSpaces.forEach(childSpace => {
@@ -489,16 +568,17 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
});
this.spaceFilteredRooms.forEach((roomIds, s) => {
- // Update NotificationStates
- this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
- if (roomIds.has(room.roomId)) {
- if (s !== HOME_SPACE && spacesTweakSpaceDMBadgesEnabled) return true;
+ if (this.allRoomsInHome && s === HOME_SPACE) return; // we'll be using the global notification state, skip
- return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
- || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
+ // Update NotificationStates
+ this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
+ if (!roomIds.has(room.roomId)) return false;
+
+ if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
+ return s === HOME_SPACE;
}
- return false;
+ return true;
}));
});
}, 100, { trailing: true, leading: true });
@@ -591,7 +671,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// TODO confirm this after implementing parenting behaviour
if (room.isSpaceRoom()) {
this.onSpaceUpdate();
- } else if (!spacesTweakAllRoomsEnabled) {
+ } else if (!this.allRoomsInHome) {
this.onRoomUpdate(room);
}
this.emit(room.roomId);
@@ -615,7 +695,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (order !== lastOrder) {
this.notifyIfOrderChanged();
}
- } else if (ev.getType() === EventType.Tag && !spacesTweakAllRoomsEnabled) {
+ } else if (ev.getType() === EventType.Tag && !this.allRoomsInHome) {
// If the room was in favourites and now isn't or the opposite then update its position in the trees
const oldTags = lastEv?.getContent()?.tags || {};
const newTags = ev.getContent()?.tags || {};
@@ -626,7 +706,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
};
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
- if (ev.getType() === EventType.Direct) {
+ if (!this.allRoomsInHome && ev.getType() === EventType.Direct) {
const lastContent = lastEvent.getContent();
const content = ev.getContent();
@@ -661,9 +741,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
- if (!spacesTweakAllRoomsEnabled) {
- this.matrixClient.removeListener("accountData", this.onAccountData);
- }
+ this.matrixClient.removeListener("accountData", this.onAccountData);
}
await this.reset();
}
@@ -674,9 +752,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.matrixClient.on("Room.myMembership", this.onRoom);
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
this.matrixClient.on("RoomState.events", this.onRoomState);
- if (!spacesTweakAllRoomsEnabled) {
- this.matrixClient.on("accountData", this.onAccountData);
- }
+ this.matrixClient.on("accountData", this.onAccountData);
+
+ this.matrixClient.getCapabilities().then(capabilities => {
+ this._restrictedJoinRuleSupport = capabilities
+ ?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"];
+ });
await this.onSpaceUpdate(); // trigger an initial update
@@ -702,7 +783,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// as it will cause you to end up in the wrong room
this.setActiveSpace(room, false);
} else if (
- (!spacesTweakAllRoomsEnabled || this.activeSpace) &&
+ (!this.allRoomsInHome || this.activeSpace) &&
!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
) {
this.switchToRelatedSpace(roomId);
@@ -714,17 +795,33 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id);
break;
}
+
case "after_leave_room":
if (this._activeSpace && payload.room_id === this._activeSpace.roomId) {
this.setActiveSpace(null, false);
}
break;
+
case Action.SwitchSpace:
if (payload.num === 0) {
this.setActiveSpace(null);
} else if (this.spacePanelSpaces.length >= payload.num) {
this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]);
}
+ break;
+
+ case Action.SettingUpdated: {
+ const settingUpdatedPayload = payload as SettingUpdatedPayload;
+ if (settingUpdatedPayload.settingName === "Spaces.allRoomsInHome") {
+ const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
+ if (this.allRoomsInHome !== newValue) {
+ this._allRoomsInHome = newValue;
+ this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
+ this.rebuild(); // rebuild everything
+ }
+ }
+ break;
+ }
}
}
@@ -795,9 +892,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
export default class SpaceStore {
public static spacesEnabled = spacesEnabled;
- public static spacesTweakAllRoomsEnabled = spacesTweakAllRoomsEnabled;
- public static spacesTweakSpaceMemberDMsEnabled = spacesTweakSpaceMemberDMsEnabled;
- public static spacesTweakSpaceDMBadgesEnabled = spacesTweakSpaceDMBadgesEnabled;
private static internalInstance = new SpaceStoreClass();
diff --git a/src/stores/VoiceRecordingStore.ts b/src/stores/VoiceRecordingStore.ts
index 81c19e7e82..df837fec88 100644
--- a/src/stores/VoiceRecordingStore.ts
+++ b/src/stores/VoiceRecordingStore.ts
@@ -17,7 +17,7 @@ limitations under the License.
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { ActionPayload } from "../dispatcher/payloads";
-import { VoiceRecording } from "../voice/VoiceRecording";
+import { VoiceRecording } from "../audio/VoiceRecording";
interface IState {
recording?: VoiceRecording;
diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts
index 4c0a582f3f..f8eb07251b 100644
--- a/src/stores/notifications/SpaceNotificationState.ts
+++ b/src/stores/notifications/SpaceNotificationState.ts
@@ -23,7 +23,7 @@ import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationStat
import { FetchRoomFn } from "./ListNotificationState";
export class SpaceNotificationState extends NotificationState {
- private rooms: Room[] = [];
+ public rooms: Room[] = []; // exposed only for tests
private states: { [spaceId: string]: RoomNotificationState } = {};
constructor(private spaceId: string | symbol, private getRoomFn: FetchRoomFn) {
diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index 3913a2220f..1a5ef0484e 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -36,6 +36,8 @@ import { RoomNotificationStateStore } from "../notifications/RoomNotificationSta
import { VisibilityProvider } from "./filters/VisibilityProvider";
import { SpaceWatcher } from "./SpaceWatcher";
import SpaceStore from "../SpaceStore";
+import { Action } from "../../dispatcher/actions";
+import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
interface IState {
tagsEnabled?: boolean;
@@ -213,10 +215,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient {
const logicallyReady = this.matrixClient && this.initialListsGenerated;
if (!logicallyReady) return;
- if (payload.action === 'setting_updated') {
- if (this.watchedSettings.includes(payload.settingName)) {
+ if (payload.action === Action.SettingUpdated) {
+ const settingUpdatedPayload = payload as SettingUpdatedPayload;
+ if (this.watchedSettings.includes(settingUpdatedPayload.settingName)) {
// TODO: Remove with https://github.com/vector-im/element-web/issues/14602
- if (payload.settingName === "advancedRoomListLogging") {
+ if (settingUpdatedPayload.settingName === "advancedRoomListLogging") {
// Log when the setting changes so we know when it was turned on in the rageshake
const enabled = SettingsStore.getValue("advancedRoomListLogging");
console.warn("Advanced room list logging is enabled? " + enabled);
@@ -708,6 +711,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient {
}
let promise = Promise.resolve();
let idx = this.filterConditions.indexOf(filter);
+ let removed = false;
if (idx >= 0) {
this.filterConditions.splice(idx, 1);
@@ -718,14 +722,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient {
if (SpaceStore.spacesEnabled) {
promise = this.recalculatePrefiltering();
}
+ removed = true;
}
+
idx = this.prefilterConditions.indexOf(filter);
if (idx >= 0) {
filter.off(FILTER_CHANGED, this.onPrefilterUpdated);
this.prefilterConditions.splice(idx, 1);
promise = this.recalculatePrefiltering();
+ removed = true;
+ }
+
+ if (removed) {
+ promise.then(() => this.updateFn.trigger());
}
- promise.then(() => this.updateFn.trigger());
}
/**
diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts
index 1cec612e6f..fe2eb1e881 100644
--- a/src/stores/room-list/SpaceWatcher.ts
+++ b/src/stores/room-list/SpaceWatcher.ts
@@ -18,39 +18,47 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomListStoreClass } from "./RoomListStore";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
-import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
+import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../SpaceStore";
/**
* Watches for changes in spaces to manage the filter on the provided RoomListStore
*/
export class SpaceWatcher {
- private filter: SpaceFilterCondition;
+ private readonly filter = new SpaceFilterCondition();
+ // we track these separately to the SpaceStore as we need to observe transitions
private activeSpace: Room = SpaceStore.instance.activeSpace;
+ private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
constructor(private store: RoomListStoreClass) {
- if (!SpaceStore.spacesTweakAllRoomsEnabled) {
- this.filter = new SpaceFilterCondition();
+ if (!this.allRoomsInHome || this.activeSpace) {
this.updateFilter();
store.addFilter(this.filter);
}
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
+ SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated);
}
- private onSelectedSpaceUpdated = (activeSpace?: Room) => {
- this.activeSpace = activeSpace;
+ private onSelectedSpaceUpdated = (activeSpace?: Room, allRoomsInHome = this.allRoomsInHome) => {
+ if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop
- if (this.filter) {
- if (activeSpace || !SpaceStore.spacesTweakAllRoomsEnabled) {
- this.updateFilter();
- } else {
- this.store.removeFilter(this.filter);
- this.filter = null;
- }
- } else if (activeSpace) {
- this.filter = new SpaceFilterCondition();
+ const oldActiveSpace = this.activeSpace;
+ const oldAllRoomsInHome = this.allRoomsInHome;
+ this.activeSpace = activeSpace;
+ this.allRoomsInHome = allRoomsInHome;
+
+ if (activeSpace || !allRoomsInHome) {
this.updateFilter();
- this.store.addFilter(this.filter);
}
+
+ if (oldAllRoomsInHome && !oldActiveSpace) {
+ this.store.addFilter(this.filter);
+ } else if (allRoomsInHome && !activeSpace) {
+ this.store.removeFilter(this.filter);
+ }
+ };
+
+ private onHomeBehaviourUpdated = (allRoomsInHome: boolean) => {
+ this.onSelectedSpaceUpdated(this.activeSpace, allRoomsInHome);
};
private updateFilter = () => {
diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts
index 04fb92f0c1..961f27fda1 100644
--- a/src/stores/room-list/previews/MessageEventPreview.ts
+++ b/src/stores/room-list/previews/MessageEventPreview.ts
@@ -17,7 +17,7 @@ limitations under the License.
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import { _t } from "../../../languageHandler";
+import { _t, sanitizeForTranslation } from "../../../languageHandler";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import ReplyThread from "../../../components/views/elements/ReplyThread";
import { getHtmlText } from "../../../HtmlUtils";
@@ -58,6 +58,8 @@ export class MessageEventPreview implements IPreview {
body = getHtmlText(body);
}
+ body = sanitizeForTranslation(body);
+
if (msgtype === 'm.emote') {
return _t("* %(senderName)s %(emote)s", { senderName: getSenderName(event), emote: body });
}
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 24869b5edc..daa1e0e787 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -54,6 +54,7 @@ import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ELEMENT_CLIENT_ID } from "../../identifiers";
import { getUserLanguage } from "../../languageHandler";
+import { WidgetVariableCustomisations } from "../../customisations/WidgetVariables";
// TODO: Destroy all of this code
@@ -191,7 +192,8 @@ export class StopGapWidget extends EventEmitter {
}
private runUrlTemplate(opts = { asPopout: false }): string {
- const templated = this.mockWidget.getCompleteUrl({
+ const fromCustomisation = WidgetVariableCustomisations?.provideVariables?.() ?? {};
+ const defaults: ITemplateParams = {
widgetRoomId: this.roomId,
currentUserId: MatrixClientPeg.get().getUserId(),
userDisplayName: OwnProfileStore.instance.displayName,
@@ -199,7 +201,8 @@ export class StopGapWidget extends EventEmitter {
clientId: ELEMENT_CLIENT_ID,
clientTheme: SettingsStore.getValue("theme"),
clientLanguage: getUserLanguage(),
- }, opts?.asPopout);
+ };
+ const templated = this.mockWidget.getCompleteUrl(Object.assign(defaults, fromCustomisation), opts?.asPopout);
const parsed = new URL(templated);
@@ -363,6 +366,9 @@ export class StopGapWidget extends EventEmitter {
}
public async prepare(): Promise {
+ // Ensure the variables are ready for us to be rendered before continuing
+ await (WidgetVariableCustomisations?.isReady?.() ?? Promise.resolve());
+
if (this.scalarToken) return;
const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget);
if (existingMessaging) this.messaging = existingMessaging;
diff --git a/src/utils/FileDownloader.ts b/src/utils/FileDownloader.ts
new file mode 100644
index 0000000000..a22ff506de
--- /dev/null
+++ b/src/utils/FileDownloader.ts
@@ -0,0 +1,103 @@
+/*
+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.
+*/
+
+export type getIframeFn = () => HTMLIFrameElement; // eslint-disable-line @typescript-eslint/naming-convention
+
+export const DEFAULT_STYLES = {
+ imgSrc: "",
+ imgStyle: null, // css props
+ style: "",
+ textContent: "",
+};
+
+type DownloadOptions = {
+ blob: Blob;
+ name: string;
+ autoDownload?: boolean;
+ opts?: typeof DEFAULT_STYLES;
+};
+
+// set up the iframe as a singleton so we don't have to figure out destruction of it down the line.
+let managedIframe: HTMLIFrameElement;
+let onLoadPromise: Promise;
+function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise } {
+ if (managedIframe) return { iframe: managedIframe, onLoadPromise };
+
+ managedIframe = document.createElement("iframe");
+
+ // Need to append the iframe in order for the browser to load it.
+ document.body.appendChild(managedIframe);
+
+ // Dev note: the reassignment warnings are entirely incorrect here.
+
+ // @ts-ignore
+ // noinspection JSConstantReassignment
+ managedIframe.style = { display: "none" };
+ // @ts-ignore
+ // noinspection JSConstantReassignment
+ managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
+
+ onLoadPromise = new Promise(resolve => {
+ managedIframe.onload = () => {
+ resolve();
+ };
+ managedIframe.src = "usercontent/"; // XXX: Should come from the skin
+ });
+
+ return { iframe: managedIframe, onLoadPromise };
+}
+
+// TODO: If we decide to keep the download link behaviour, we should bring the style management into here.
+
+/**
+ * Helper to handle safe file downloads. This operates off an iframe for reasons described
+ * by the blob helpers. By default, this will use a hidden iframe to manage the download
+ * through a user content wrapper, but can be given an iframe reference if the caller needs
+ * additional control over the styling/position of the iframe itself.
+ */
+export class FileDownloader {
+ private onLoadPromise: Promise;
+
+ /**
+ * Creates a new file downloader
+ * @param iframeFn Function to get a pre-configured iframe. Set to null to have the downloader
+ * use a generic, hidden, iframe.
+ */
+ constructor(private iframeFn: getIframeFn = null) {
+ }
+
+ private get iframe(): HTMLIFrameElement {
+ const iframe = this.iframeFn?.();
+ if (!iframe) {
+ const managed = getManagedIframe();
+ this.onLoadPromise = managed.onLoadPromise;
+ return managed.iframe;
+ }
+ this.onLoadPromise = null;
+ return iframe;
+ }
+
+ public async download({ blob, name, autoDownload = true, opts = DEFAULT_STYLES }: DownloadOptions) {
+ const iframe = this.iframe; // get the iframe first just in case we need to await onload
+ if (this.onLoadPromise) await this.onLoadPromise;
+ iframe.contentWindow.postMessage({
+ ...opts,
+ blob: blob,
+ download: name,
+ auto: autoDownload,
+ }, '*');
+ }
+}
diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts
index 355fa2135c..c83f2ed417 100644
--- a/src/utils/FileUtils.ts
+++ b/src/utils/FileUtils.ts
@@ -26,12 +26,14 @@ import { _t } from '../languageHandler';
* @param {IMediaEventContent} content The "content" key of the matrix event.
* @param {string} fallbackText The fallback text
* @param {boolean} withSize Whether to include size information. Default true.
+ * @param {boolean} shortened Ensure the extension of the file name is visible. Default false.
* @return {string} the human readable link text for the attachment.
*/
export function presentableTextForFile(
content: IMediaEventContent,
fallbackText = _t("Attachment"),
withSize = true,
+ shortened = false,
): string {
let text = fallbackText;
if (content.body && content.body.length > 0) {
@@ -40,6 +42,21 @@ export function presentableTextForFile(
text = content.body;
}
+ // We shorten to 15 characters somewhat arbitrarily, and assume most files
+ // will have a 3 character (plus full stop) extension. The goal is to knock
+ // the label down to 15-25 characters, not perfect accuracy.
+ if (shortened && text.length > 19) {
+ const parts = text.split('.');
+ let fileName = parts.slice(0, parts.length - 1).join('.').substring(0, 15);
+ const extension = parts[parts.length - 1];
+
+ // Trim off any full stops from the file name to avoid a case where we
+ // add an ellipsis that looks really funky.
+ fileName = fileName.replace(/\.*$/g, '');
+
+ text = `${fileName}...${extension}`;
+ }
+
if (content.info && content.info.size && withSize) {
// If we know the size of the file then add it as human readable
// string to the end of the link text so that the user knows how
diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts
index cf34d5dea4..8b8edcc62a 100644
--- a/src/utils/MediaEventHelper.ts
+++ b/src/utils/MediaEventHelper.ts
@@ -67,6 +67,7 @@ export class MediaEventHelper implements IDestroyable {
private prepareThumbnailUrl = async () => {
if (this.media.isEncrypted) {
const blob = await this.thumbnailBlob.value;
+ if (blob === null) return null;
return URL.createObjectURL(blob);
} else {
return this.media.thumbnailHttp;
diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts
new file mode 100644
index 0000000000..e632ec6345
--- /dev/null
+++ b/src/utils/RoomUpgrade.ts
@@ -0,0 +1,93 @@
+/*
+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 { Room } from "matrix-js-sdk/src/models/room";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import { inviteUsersToRoom } from "../RoomInvite";
+import Modal from "../Modal";
+import { _t } from "../languageHandler";
+import ErrorDialog from "../components/views/dialogs/ErrorDialog";
+import SpaceStore from "../stores/SpaceStore";
+
+export async function upgradeRoom(
+ room: Room,
+ targetVersion: string,
+ inviteUsers = false,
+ handleError = true,
+ updateSpaces = true,
+): Promise {
+ const cli = room.client;
+
+ let newRoomId: string;
+ try {
+ ({ replacement_room: newRoomId } = await cli.upgradeRoom(room.roomId, targetVersion));
+ } catch (e) {
+ if (!handleError) throw e;
+ console.error(e);
+
+ Modal.createTrackedDialog("Room Upgrade Error", "", ErrorDialog, {
+ title: _t('Error upgrading room'),
+ description: _t('Double check that your server supports the room version chosen and try again.'),
+ });
+ throw e;
+ }
+
+ // We have to wait for the js-sdk to give us the room back so
+ // we can more effectively abuse the MultiInviter behaviour
+ // which heavily relies on the Room object being available.
+ if (inviteUsers) {
+ const checkForUpgradeFn = async (newRoom: Room): Promise => {
+ // The upgradePromise should be done by the time we await it here.
+ if (newRoom.roomId !== newRoomId) return;
+
+ const toInvite = [
+ ...room.getMembersWithMembership("join"),
+ ...room.getMembersWithMembership("invite"),
+ ].map(m => m.userId).filter(m => m !== cli.getUserId());
+
+ if (toInvite.length > 0) {
+ // Errors are handled internally to this function
+ await inviteUsersToRoom(newRoomId, toInvite);
+ }
+
+ cli.removeListener('Room', checkForUpgradeFn);
+ };
+ cli.on('Room', checkForUpgradeFn);
+ }
+
+ if (updateSpaces) {
+ const parents = SpaceStore.instance.getKnownParents(room.roomId);
+ try {
+ for (const parentId of parents) {
+ const parent = cli.getRoom(parentId);
+ if (!parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())) continue;
+
+ const currentEv = parent.currentState.getStateEvents(EventType.SpaceChild, room.roomId);
+ await cli.sendStateEvent(parentId, EventType.SpaceChild, {
+ ...(currentEv?.getContent() || {}), // copy existing attributes like suggested
+ via: [cli.getDomain()],
+ }, newRoomId);
+ await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, room.roomId);
+ }
+ } catch (e) {
+ // These errors are not critical to the room upgrade itself
+ console.warn("Failed to update parent spaces during room upgrade", e);
+ }
+ }
+
+ return newRoomId;
+}
diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index c2ee6ce100..e3b7b6cf59 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -141,21 +141,3 @@ export function objectKeyChanges(a: O, b: O): (keyof O)[] {
export function objectClone(obj: O): O {
return JSON.parse(JSON.stringify(obj));
}
-
-/**
- * Converts a series of entries to an object.
- * @param entries The entries to convert.
- * @returns The converted object.
- */
-// NOTE: Deprecated once we have Object.fromEntries() support.
-// @ts-ignore - return type is complaining about non-string keys, but we know better
-export function objectFromEntries(entries: Iterable<[K, V]>): {[k: K]: V} {
- const obj: {
- // @ts-ignore - same as return type
- [k: K]: V;} = {};
- for (const e of entries) {
- // @ts-ignore - same as return type
- obj[e[0]] = e[1];
- }
- return obj;
-}
diff --git a/src/utils/space.tsx b/src/utils/space.tsx
index 38f6e348d7..fecb581e65 100644
--- a/src/utils/space.tsx
+++ b/src/utils/space.tsx
@@ -16,10 +16,9 @@ limitations under the License.
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
-import { calculateRoomVia } from "../utils/permalinks/Permalinks";
+import { calculateRoomVia } from "./permalinks/Permalinks";
import Modal from "../Modal";
import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog";
@@ -29,9 +28,18 @@ import { _t } from "../languageHandler";
import SpacePublicShare from "../components/views/spaces/SpacePublicShare";
import InfoDialog from "../components/views/dialogs/InfoDialog";
import { showRoomInviteDialog } from "../RoomInvite";
+import CreateSubspaceDialog from "../components/views/dialogs/CreateSubspaceDialog";
+import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSubspaceDialog";
+import defaultDispatcher from "../dispatcher/dispatcher";
+import RoomViewStore from "../stores/RoomViewStore";
+import { Action } from "../dispatcher/actions";
+import { leaveRoomBehaviour } from "./membership";
+import Spinner from "../components/views/elements/Spinner";
+import dis from "../dispatcher/dispatcher";
+import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog";
-export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
- const userId = cli.getUserId();
+export const shouldShowSpaceSettings = (space: Room) => {
+ const userId = space.client.getUserId();
return space.getMyMembership() === "join"
&& (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
|| space.currentState.maySendStateEvent(EventType.RoomName, userId)
@@ -48,28 +56,33 @@ export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
state_key: room.roomId,
});
-export const showSpaceSettings = (cli: MatrixClient, space: Room) => {
+export const showSpaceSettings = (space: Room) => {
Modal.createTrackedDialog("Space Settings", "", SpaceSettingsDialog, {
- matrixClient: cli,
+ matrixClient: space.client,
space,
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
};
-export const showAddExistingRooms = async (cli: MatrixClient, space: Room) => {
- return Modal.createTrackedDialog(
+export const showAddExistingRooms = (space: Room): void => {
+ Modal.createTrackedDialog(
"Space Landing",
"Add Existing",
AddExistingToSpaceDialog,
{
- matrixClient: cli,
- onCreateRoomClick: showCreateNewRoom,
+ onCreateRoomClick: () => showCreateNewRoom(space),
+ onAddSubspaceClick: () => showAddExistingSubspace(space),
space,
+ onFinished: (added: boolean) => {
+ if (added && RoomViewStore.getRoomId() === space.roomId) {
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
+ }
+ },
},
"mx_AddExistingToSpaceDialog_wrapper",
- ).finished;
+ );
};
-export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
+export const showCreateNewRoom = async (space: Room): Promise => {
const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
"Space Landing",
"Create Room",
@@ -86,7 +99,7 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
return shouldCreate;
};
-export const showSpaceInvite = (space: Room, initialText = "") => {
+export const showSpaceInvite = (space: Room, initialText = ""): void => {
if (space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite to %(spaceName)s", { spaceName: space.name }),
@@ -103,3 +116,60 @@ export const showSpaceInvite = (space: Room, initialText = "") => {
showRoomInviteDialog(space.roomId, initialText);
}
};
+
+export const showAddExistingSubspace = (space: Room): void => {
+ Modal.createTrackedDialog(
+ "Space Landing",
+ "Create Subspace",
+ AddExistingSubspaceDialog,
+ {
+ space,
+ onCreateSubspaceClick: () => showCreateNewSubspace(space),
+ onFinished: (added: boolean) => {
+ if (added && RoomViewStore.getRoomId() === space.roomId) {
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
+ }
+ },
+ },
+ "mx_AddExistingToSpaceDialog_wrapper",
+ );
+};
+
+export const showCreateNewSubspace = (space: Room): void => {
+ Modal.createTrackedDialog(
+ "Space Landing",
+ "Create Subspace",
+ CreateSubspaceDialog,
+ {
+ space,
+ onAddExistingSpaceClick: () => showAddExistingSubspace(space),
+ onFinished: (added: boolean) => {
+ if (added && RoomViewStore.getRoomId() === space.roomId) {
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
+ }
+ },
+ },
+ "mx_CreateSubspaceDialog_wrapper",
+ );
+};
+
+export const leaveSpace = (space: Room) => {
+ Modal.createTrackedDialog("Leave Space", "", LeaveSpaceDialog, {
+ space,
+ onFinished: async (leave: boolean, rooms: Room[]) => {
+ if (!leave) return;
+ const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
+ try {
+ await Promise.all(rooms.map(r => leaveRoomBehaviour(r.roomId)));
+ await leaveRoomBehaviour(space.roomId);
+ } finally {
+ modal.close();
+ }
+
+ dis.dispatch({
+ action: "after_leave_room",
+ room_id: space.roomId,
+ });
+ },
+ }, "mx_LeaveSpaceDialog_wrapper");
+};
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index f415b85105..dea5bcefb7 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -312,8 +312,12 @@ describe('MessagePanel', function() {
it('should insert the read-marker in the right place', function() {
const res = TestUtils.renderIntoDocument(
- ,
+ ,
);
const tiles = TestUtils.scryRenderedComponentsWithType(
@@ -330,8 +334,12 @@ describe('MessagePanel', function() {
it('should show the read-marker that fall in summarised events after the summary', function() {
const melsEvents = mkMelsEvents();
const res = TestUtils.renderIntoDocument(
- ,
+ ,
);
const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary');
@@ -348,8 +356,12 @@ describe('MessagePanel', function() {
it('should hide the read-marker at the end of summarised events', function() {
const melsEvents = mkMelsEventsOnly();
const res = TestUtils.renderIntoDocument(
- ,
+ ,
);
const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary');
@@ -371,7 +383,10 @@ describe('MessagePanel', function() {
// first render with the RM in one place
let mp = ReactDOM.render(
- , parentDiv);
@@ -387,7 +402,10 @@ describe('MessagePanel', function() {
// now move the RM
mp = ReactDOM.render(
- , parentDiv);
diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js
index e2d51f13a4..dcb895f09e 100644
--- a/test/components/views/elements/MemberEventListSummary-test.js
+++ b/test/components/views/elements/MemberEventListSummary-test.js
@@ -106,7 +106,7 @@ describe('MemberEventListSummary', function() {
const result = wrapper.props.children;
expect(result.props.children).toEqual([
- Expanded membership
,
+ Expanded membership
,
]);
});
@@ -129,8 +129,8 @@ describe('MemberEventListSummary', function() {
const result = wrapper.props.children;
expect(result.props.children).toEqual([
- Expanded membership
,
- Expanded membership
,
+ Expanded membership
,
+ Expanded membership
,
]);
});
diff --git a/test/end-to-end-tests/src/scenarios/directory.js b/test/end-to-end-tests/src/scenarios/directory.js
index 53b790c174..fffca2b05c 100644
--- a/test/end-to-end-tests/src/scenarios/directory.js
+++ b/test/end-to-end-tests/src/scenarios/directory.js
@@ -25,7 +25,7 @@ module.exports = async function roomDirectoryScenarios(alice, bob) {
console.log(" creating a public room and join through directory:");
const room = 'test';
await createRoom(alice, room);
- await changeRoomSettings(alice, { directory: true, visibility: "public_no_guests", alias: "#test" });
+ await changeRoomSettings(alice, { directory: true, visibility: "public", alias: "#test" });
await join(bob, room); //looks up room in directory
const bobMessage = "hi Alice!";
await sendMessage(bob, bobMessage);
diff --git a/test/end-to-end-tests/src/scenarios/lazy-loading.js b/test/end-to-end-tests/src/scenarios/lazy-loading.js
index 1b5d449af9..406f7b24a3 100644
--- a/test/end-to-end-tests/src/scenarios/lazy-loading.js
+++ b/test/end-to-end-tests/src/scenarios/lazy-loading.js
@@ -51,7 +51,7 @@ const charlyMsg2 = "how's it going??";
async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) {
await createRoom(bob, room);
- await changeRoomSettings(bob, { directory: true, visibility: "public_no_guests", alias });
+ await changeRoomSettings(bob, { directory: true, visibility: "public", alias });
// wait for alias to be set by server after clicking "save"
// so the charlies can join it.
await bob.delay(500);
diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js
index b40afe76bf..01431197a7 100644
--- a/test/end-to-end-tests/src/usecases/room-settings.js
+++ b/test/end-to-end-tests/src/usecases/room-settings.js
@@ -98,18 +98,14 @@ async function checkRoomSettings(session, expectedSettings) {
if (expectedSettings.visibility) {
session.log.step(`checks visibility is ${expectedSettings.visibility}`);
const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]");
- assert.equal(radios.length, 7);
- const inviteOnly = radios[0];
- const publicNoGuests = radios[1];
- const publicWithGuests = radios[2];
+ assert.equal(radios.length, 6);
+ const [inviteOnlyRoom, publicRoom] = radios;
let expectedRadio = null;
if (expectedSettings.visibility === "invite_only") {
- expectedRadio = inviteOnly;
- } else if (expectedSettings.visibility === "public_no_guests") {
- expectedRadio = publicNoGuests;
- } else if (expectedSettings.visibility === "public_with_guests") {
- expectedRadio = publicWithGuests;
+ expectedRadio = inviteOnlyRoom;
+ } else if (expectedSettings.visibility === "public") {
+ expectedRadio = publicRoom;
} else {
throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`);
}
@@ -165,17 +161,13 @@ async function changeRoomSettings(session, settings) {
if (settings.visibility) {
session.log.step(`sets visibility to ${settings.visibility}`);
const radios = await session.queryAll(".mx_RoomSettingsDialog label");
- assert.equal(radios.length, 7);
- const inviteOnly = radios[0];
- const publicNoGuests = radios[1];
- const publicWithGuests = radios[2];
+ assert.equal(radios.length, 6);
+ const [inviteOnlyRoom, publicRoom] = radios;
if (settings.visibility === "invite_only") {
- await inviteOnly.click();
- } else if (settings.visibility === "public_no_guests") {
- await publicNoGuests.click();
- } else if (settings.visibility === "public_with_guests") {
- await publicWithGuests.click();
+ await inviteOnlyRoom.click();
+ } else if (settings.visibility === "public") {
+ await publicRoom.click();
} else {
throw new Error(`unrecognized room visibility setting: ${settings.visibility}`);
}
diff --git a/test/notifications/ContentRules-test.js b/test/notifications/ContentRules-test.js
index 9c21c05da7..2b18a18488 100644
--- a/test/notifications/ContentRules-test.js
+++ b/test/notifications/ContentRules-test.js
@@ -56,7 +56,7 @@ describe("ContentRules", function() {
describe("parseContentRules", function() {
it("should handle there being no keyword rules", function() {
const rules = { 'global': { 'content': [
- USERNAME_RULE,
+ USERNAME_RULE,
] } };
const parsed = ContentRules.parseContentRules(rules);
expect(parsed.rules).toEqual([]);
diff --git a/test/stores/SpaceStore-setup.ts b/test/stores/SpaceStore-setup.ts
index 67d492255f..78418d45cc 100644
--- a/test/stores/SpaceStore-setup.ts
+++ b/test/stores/SpaceStore-setup.ts
@@ -18,6 +18,3 @@ limitations under the License.
// SpaceStore reads the SettingsStore which needs the localStorage values set at init time.
localStorage.setItem("mx_labs_feature_feature_spaces", "true");
-localStorage.setItem("mx_labs_feature_feature_spaces.all_rooms", "true");
-localStorage.setItem("mx_labs_feature_feature_spaces.space_member_dms", "true");
-localStorage.setItem("mx_labs_feature_feature_spaces.space_dm_badges", "false");
diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts
index eb28a72d67..2e823aa72b 100644
--- a/test/stores/SpaceStore-test.ts
+++ b/test/stores/SpaceStore-test.ts
@@ -16,81 +16,45 @@ limitations under the License.
import { EventEmitter } from "events";
import { EventType } from "matrix-js-sdk/src/@types/event";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import "./SpaceStore-setup"; // enable space lab
import "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore, {
+ UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../src/stores/SpaceStore";
-import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils";
-import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
-import { EnhancedMap } from "../../src/utils/maps";
+import * as testUtils from "../utils/test-utils";
+import { mkEvent, stubClient } from "../test-utils";
import DMRoomMap from "../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import defaultDispatcher from "../../src/dispatcher/dispatcher";
+import SettingsStore from "../../src/settings/SettingsStore";
+import { SettingLevel } from "../../src/settings/SettingLevel";
jest.useFakeTimers();
-const mockStateEventImplementation = (events: MatrixEvent[]) => {
- const stateMap = new EnhancedMap>();
- events.forEach(event => {
- stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event);
- });
-
- return (eventType: string, stateKey?: string) => {
- if (stateKey || stateKey === "") {
- return stateMap.get(eventType)?.get(stateKey) || null;
- }
- return Array.from(stateMap.get(eventType)?.values() || []);
- };
-};
-
-const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r));
-
const testUserId = "@test:user";
-let rooms = [];
-
-const mkRoom = (roomId: string) => {
- const room = mkStubRoom(roomId);
- room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([]));
- rooms.push(room);
- return room;
-};
-
-const mkSpace = (spaceId: string, children: string[] = []) => {
- const space = mkRoom(spaceId);
- space.isSpaceRoom.mockReturnValue(true);
- space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId =>
- mkEvent({
- event: true,
- type: EventType.SpaceChild,
- room: spaceId,
- user: testUserId,
- skey: roomId,
- content: { via: [] },
- ts: Date.now(),
- }),
- )));
- return space;
-};
-
const getUserIdForRoomId = jest.fn();
+const getDMRoomsForUserId = jest.fn();
// @ts-ignore
-DMRoomMap.sharedInstance = { getUserIdForRoomId };
+DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
const fav1 = "!fav1:server";
const fav2 = "!fav2:server";
const fav3 = "!fav3:server";
const dm1 = "!dm1:server";
-const dm1Partner = "@dm1Partner:server";
+const dm1Partner = new RoomMember(dm1, "@dm1Partner:server");
+dm1Partner.membership = "join";
const dm2 = "!dm2:server";
-const dm2Partner = "@dm2Partner:server";
+const dm2Partner = new RoomMember(dm2, "@dm2Partner:server");
+dm2Partner.membership = "join";
const dm3 = "!dm3:server";
-const dm3Partner = "@dm3Partner:server";
+const dm3Partner = new RoomMember(dm3, "@dm3Partner:server");
+dm3Partner.membership = "join";
const orphan1 = "!orphan1:server";
const orphan2 = "!orphan2:server";
const invite1 = "!invite1:server";
@@ -107,20 +71,31 @@ describe("SpaceStore", () => {
const store = SpaceStore.instance;
const client = MatrixClientPeg.get();
+ let rooms = [];
+ const mkRoom = (roomId: string) => testUtils.mkRoom(client, roomId, rooms);
+ const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children);
const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true);
const run = async () => {
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
- await setupAsyncStoreWithClient(store, client);
+ await testUtils.setupAsyncStoreWithClient(store, client);
jest.runAllTimers();
};
+ const setShowAllRooms = async (value: boolean) => {
+ if (store.allRoomsInHome === value) return;
+ const emitProm = testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR);
+ await SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, value);
+ jest.runAllTimers(); // run async dispatch
+ await emitProm;
+ };
+
beforeEach(() => {
- jest.runAllTimers();
+ jest.runAllTimers(); // run async dispatch
client.getVisibleRooms.mockReturnValue(rooms = []);
});
afterEach(async () => {
- await resetAsyncStoreWithClient(store);
+ await testUtils.resetAsyncStoreWithClient(store);
});
describe("static hierarchy resolution tests", () => {
@@ -320,11 +295,40 @@ describe("SpaceStore", () => {
getUserIdForRoomId.mockImplementation(roomId => {
return {
- [dm1]: dm1Partner,
- [dm2]: dm2Partner,
- [dm3]: dm3Partner,
+ [dm1]: dm1Partner.userId,
+ [dm2]: dm2Partner.userId,
+ [dm3]: dm3Partner.userId,
}[roomId];
});
+ getDMRoomsForUserId.mockImplementation(userId => {
+ switch (userId) {
+ case dm1Partner.userId:
+ return [dm1];
+ case dm2Partner.userId:
+ return [dm2];
+ case dm3Partner.userId:
+ return [dm3];
+ default:
+ return [];
+ }
+ });
+
+ // have dmPartner1 be in space1 with you
+ const mySpace1Member = new RoomMember(space1, testUserId);
+ mySpace1Member.membership = "join";
+ (rooms.find(r => r.roomId === space1).getMembers as jest.Mock).mockReturnValue([
+ mySpace1Member,
+ dm1Partner,
+ ]);
+ // have dmPartner2 be in space2 with you
+ const mySpace2Member = new RoomMember(space2, testUserId);
+ mySpace2Member.membership = "join";
+ (rooms.find(r => r.roomId === space2).getMembers as jest.Mock).mockReturnValue([
+ mySpace2Member,
+ dm2Partner,
+ ]);
+ // dmPartner3 is not in any common spaces with you
+
await run();
});
@@ -353,10 +357,16 @@ describe("SpaceStore", () => {
expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy();
});
- it("home space does contain rooms/low priority even if they are also shown in a space", () => {
+ it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => {
+ await setShowAllRooms(true);
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy();
});
+ it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => {
+ await setShowAllRooms(false);
+ expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy();
+ });
+
it("space contains child rooms", () => {
const space = client.getRoom(space1);
expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy();
@@ -375,6 +385,66 @@ describe("SpaceStore", () => {
const space = client.getRoom(space3);
expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy();
});
+
+ it("spaces contain dms which you have with members of that space", () => {
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm2)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm2)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm3)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm3)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm3)).toBeFalsy();
+ });
+
+ it("dms are only added to Notification States for only the Home Space", () => {
+ // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better
+ // [dm1, dm2, dm3].forEach(d => {
+ // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy();
+ // });
+ [space1, space2, space3].forEach(s => {
+ [dm1, dm2, dm3].forEach(d => {
+ expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy();
+ });
+ });
+ });
+
+ it("orphan rooms are added to Notification States for only the Home Space", () => {
+ // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better
+ // [orphan1, orphan2].forEach(d => {
+ // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy();
+ // });
+ [space1, space2, space3].forEach(s => {
+ [orphan1, orphan2].forEach(d => {
+ expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy();
+ });
+ });
+ });
+
+ it("favourites are added to Notification States for all spaces containing the room inc Home", () => {
+ // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better
+ // [fav1, fav2, fav3].forEach(d => {
+ // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy();
+ // });
+ expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(fav1)).toBeTruthy();
+ expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(fav2)).toBeFalsy();
+ expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(fav3)).toBeFalsy();
+ expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(fav1)).toBeTruthy();
+ expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(fav2)).toBeTruthy();
+ expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(fav3)).toBeTruthy();
+ expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(fav1)).toBeFalsy();
+ expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(fav2)).toBeFalsy();
+ expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(fav3)).toBeFalsy();
+ });
+
+ it("other rooms are added to Notification States for all spaces containing the room exc Home", () => {
+ // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better
+ // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(room1)).toBeFalsy();
+ expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(room1)).toBeTruthy();
+ expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(room1)).toBeTruthy();
+ expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(room1)).toBeFalsy();
+ });
});
});
@@ -394,7 +464,7 @@ describe("SpaceStore", () => {
await run();
expect(store.spacePanelSpaces).toStrictEqual([]);
const space = mkSpace(space1);
- const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
+ const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
emitter.emit("Room", space);
await prom;
expect(store.spacePanelSpaces).toStrictEqual([space]);
@@ -407,7 +477,7 @@ describe("SpaceStore", () => {
expect(store.spacePanelSpaces).toStrictEqual([space]);
space.getMyMembership.mockReturnValue("leave");
- const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
+ const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
emitter.emit("Room.myMembership", space, "leave", "join");
await prom;
expect(store.spacePanelSpaces).toStrictEqual([]);
@@ -419,7 +489,7 @@ describe("SpaceStore", () => {
expect(store.invitedSpaces).toStrictEqual([]);
const space = mkSpace(space1);
space.getMyMembership.mockReturnValue("invite");
- const prom = emitPromise(store, UPDATE_INVITED_SPACES);
+ const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES);
emitter.emit("Room", space);
await prom;
expect(store.spacePanelSpaces).toStrictEqual([]);
@@ -434,7 +504,7 @@ describe("SpaceStore", () => {
expect(store.spacePanelSpaces).toStrictEqual([]);
expect(store.invitedSpaces).toStrictEqual([space]);
space.getMyMembership.mockReturnValue("join");
- const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
+ const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
emitter.emit("Room.myMembership", space, "join", "invite");
await prom;
expect(store.spacePanelSpaces).toStrictEqual([space]);
@@ -449,7 +519,7 @@ describe("SpaceStore", () => {
expect(store.spacePanelSpaces).toStrictEqual([]);
expect(store.invitedSpaces).toStrictEqual([space]);
space.getMyMembership.mockReturnValue("leave");
- const prom = emitPromise(store, UPDATE_INVITED_SPACES);
+ const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES);
emitter.emit("Room.myMembership", space, "leave", "invite");
await prom;
expect(store.spacePanelSpaces).toStrictEqual([]);
@@ -469,7 +539,7 @@ describe("SpaceStore", () => {
const invite = mkRoom(invite1);
invite.getMyMembership.mockReturnValue("invite");
- const prom = emitPromise(store, space1);
+ const prom = testUtils.emitPromise(store, space1);
emitter.emit("Room", space);
await prom;
@@ -539,20 +609,30 @@ describe("SpaceStore", () => {
});
describe("context switching tests", () => {
- const fn = jest.spyOn(defaultDispatcher, "dispatch");
+ let dispatcherRef;
+ let currentRoom = null;
beforeEach(async () => {
[room1, room2, orphan1].forEach(mkRoom);
mkSpace(space1, [room1, room2]);
mkSpace(space2, [room2]);
await run();
+
+ dispatcherRef = defaultDispatcher.register(payload => {
+ if (payload.action === "view_room" || payload.action === "view_home_page") {
+ currentRoom = payload.room_id || null;
+ }
+ });
});
afterEach(() => {
- fn.mockClear();
localStorage.clear();
+ defaultDispatcher.unregister(dispatcherRef);
});
- const getCurrentRoom = () => fn.mock.calls.reverse().find(([p]) => p.action === "view_room")?.[0].room_id;
+ const getCurrentRoom = () => {
+ jest.runAllTimers();
+ return currentRoom;
+ };
it("last viewed room in target space is the current viewed and in both spaces", async () => {
await store.setActiveSpace(client.getRoom(space1));
@@ -589,6 +669,14 @@ describe("SpaceStore", () => {
expect(getCurrentRoom()).toBe(space2);
});
+ it("last viewed room is target space is no longer in that space", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ viewRoom(room1);
+ localStorage.setItem(`mx_space_context_${space2}`, room1);
+ await store.setActiveSpace(client.getRoom(space2));
+ expect(getCurrentRoom()).toBe(space2); // Space home instead of room1
+ });
+
it("no last viewed room in target space", async () => {
await store.setActiveSpace(client.getRoom(space1));
viewRoom(room1);
@@ -600,7 +688,7 @@ describe("SpaceStore", () => {
await store.setActiveSpace(client.getRoom(space1));
viewRoom(room1);
await store.setActiveSpace(null);
- expect(fn.mock.calls[fn.mock.calls.length - 1][0]).toStrictEqual({ action: "view_home_page" });
+ expect(getCurrentRoom()).toBeNull(); // Home
});
});
@@ -610,7 +698,8 @@ describe("SpaceStore", () => {
mkSpace(space1, [room1, room2, room3]);
mkSpace(space2, [room1, room2]);
- client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([
+ const cliRoom2 = client.getRoom(room2);
+ cliRoom2.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
mkEvent({
event: true,
type: EventType.SpaceParent,
@@ -653,6 +742,7 @@ describe("SpaceStore", () => {
});
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
+ await setShowAllRooms(true);
viewRoom(room2);
await store.setActiveSpace(null, false);
viewRoom(room1);
diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts
new file mode 100644
index 0000000000..85f79c75b6
--- /dev/null
+++ b/test/stores/room-list/SpaceWatcher-test.ts
@@ -0,0 +1,186 @@
+/*
+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 "../SpaceStore-setup"; // enable space lab
+import "../../skinned-sdk"; // Must be first for skinning to work
+import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
+import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
+import SettingsStore from "../../../src/settings/SettingsStore";
+import SpaceStore, { UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/SpaceStore";
+import { stubClient } from "../../test-utils";
+import { SettingLevel } from "../../../src/settings/SettingLevel";
+import { setupAsyncStoreWithClient } from "../../utils/test-utils";
+import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
+import * as testUtils from "../../utils/test-utils";
+import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition";
+
+let filter: SpaceFilterCondition = null;
+
+const mockRoomListStore = {
+ addFilter: f => filter = f,
+ removeFilter: () => filter = null,
+} as unknown as RoomListStoreClass;
+
+const space1Id = "!space1:server";
+const space2Id = "!space2:server";
+
+describe("SpaceWatcher", () => {
+ stubClient();
+ const store = SpaceStore.instance;
+ const client = MatrixClientPeg.get();
+
+ let rooms = [];
+ const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children);
+
+ const setShowAllRooms = async (value: boolean) => {
+ if (store.allRoomsInHome === value) return;
+ await SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, value);
+ await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR);
+ };
+
+ let space1;
+ let space2;
+
+ beforeEach(async () => {
+ filter = null;
+ store.removeAllListeners();
+ await store.setActiveSpace(null);
+ client.getVisibleRooms.mockReturnValue(rooms = []);
+
+ space1 = mkSpace(space1Id);
+ space2 = mkSpace(space2Id);
+
+ client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
+ await setupAsyncStoreWithClient(store, client);
+ });
+
+ it("initialises sanely with home behaviour", async () => {
+ await setShowAllRooms(false);
+ new SpaceWatcher(mockRoomListStore);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ });
+
+ it("initialises sanely with all behaviour", async () => {
+ await setShowAllRooms(true);
+ new SpaceWatcher(mockRoomListStore);
+
+ expect(filter).toBeNull();
+ });
+
+ it("sets space=null filter for all -> home transition", async () => {
+ await setShowAllRooms(true);
+ new SpaceWatcher(mockRoomListStore);
+
+ await setShowAllRooms(false);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBeNull();
+ });
+
+ it("sets filter correctly for all -> space transition", async () => {
+ await setShowAllRooms(true);
+ new SpaceWatcher(mockRoomListStore);
+
+ await SpaceStore.instance.setActiveSpace(space1);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ });
+
+ it("removes filter for home -> all transition", async () => {
+ await setShowAllRooms(false);
+ new SpaceWatcher(mockRoomListStore);
+
+ await setShowAllRooms(true);
+
+ expect(filter).toBeNull();
+ });
+
+ it("sets filter correctly for home -> space transition", async () => {
+ await setShowAllRooms(false);
+ new SpaceWatcher(mockRoomListStore);
+
+ await SpaceStore.instance.setActiveSpace(space1);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ });
+
+ it("removes filter for space -> all transition", async () => {
+ await setShowAllRooms(true);
+ new SpaceWatcher(mockRoomListStore);
+
+ await SpaceStore.instance.setActiveSpace(space1);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ await SpaceStore.instance.setActiveSpace(null);
+
+ expect(filter).toBeNull();
+ });
+
+ it("updates filter correctly for space -> home transition", async () => {
+ await setShowAllRooms(false);
+ await SpaceStore.instance.setActiveSpace(space1);
+
+ new SpaceWatcher(mockRoomListStore);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ await SpaceStore.instance.setActiveSpace(null);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(null);
+ });
+
+ it("updates filter correctly for space -> space transition", async () => {
+ await setShowAllRooms(false);
+ await SpaceStore.instance.setActiveSpace(space1);
+
+ new SpaceWatcher(mockRoomListStore);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ await SpaceStore.instance.setActiveSpace(space2);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space2);
+ });
+
+ it("doesn't change filter when changing showAllRooms mode to true", async () => {
+ await setShowAllRooms(false);
+ await SpaceStore.instance.setActiveSpace(space1);
+
+ new SpaceWatcher(mockRoomListStore);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ await setShowAllRooms(true);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ });
+
+ it("doesn't change filter when changing showAllRooms mode to false", async () => {
+ await setShowAllRooms(true);
+ await SpaceStore.instance.setActiveSpace(space1);
+
+ new SpaceWatcher(mockRoomListStore);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ await setShowAllRooms(false);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ });
+});
diff --git a/test/test-utils.js b/test/test-utils.js
index d75abc80f0..217c399443 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -97,6 +97,7 @@ export function createTestClient() {
},
decryptEventIfNeeded: () => Promise.resolve(),
isUserIgnored: jest.fn().mockReturnValue(false),
+ getCapabilities: jest.fn().mockResolvedValue({}),
};
}
@@ -128,8 +129,8 @@ export function mkEvent(opts) {
if (opts.skey) {
event.state_key = opts.skey;
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
- "m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption",
- "com.example.state"].indexOf(opts.type) !== -1) {
+ "m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption",
+ "com.example.state"].indexOf(opts.type) !== -1) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
@@ -220,7 +221,7 @@ export function mkMessage(opts) {
return mkEvent(opts);
}
-export function mkStubRoom(roomId = null, name) {
+export function mkStubRoom(roomId = null, name, client) {
const stubTimeline = { getEvents: () => [] };
return {
roomId,
@@ -235,6 +236,7 @@ export function mkStubRoom(roomId = null, name) {
}),
getMembersWithMembership: jest.fn().mockReturnValue([]),
getJoinedMembers: jest.fn().mockReturnValue([]),
+ getJoinedMemberCount: jest.fn().mockReturnValue(1),
getMembers: jest.fn().mockReturnValue([]),
getPendingEvents: () => [],
getLiveTimeline: () => stubTimeline,
@@ -269,6 +271,8 @@ export function mkStubRoom(roomId = null, name) {
getCanonicalAlias: jest.fn(),
getAltAliases: jest.fn().mockReturnValue([]),
timeline: [],
+ getJoinRule: jest.fn().mockReturnValue("invite"),
+ client,
};
}
diff --git a/test/utils/ShieldUtils-test.js b/test/utils/ShieldUtils-test.js
index b70031dc21..85f9de3150 100644
--- a/test/utils/ShieldUtils-test.js
+++ b/test/utils/ShieldUtils-test.js
@@ -49,7 +49,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
it.each(
[[true, true], [true, false],
- [false, true], [false, false]],
+ [false, true], [false, false]],
)("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => {
const client = mkClient(trusted);
const room = {
@@ -62,7 +62,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
it.each(
[["verified", true, true], ["verified", true, false],
- ["verified", false, true], ["warning", false, false]],
+ ["verified", false, true], ["warning", false, false]],
)("2 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
@@ -75,7 +75,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
it.each(
[["normal", true, true], ["normal", true, false],
- ["normal", false, true], ["warning", false, false]],
+ ["normal", false, true], ["warning", false, false]],
)("2 mixed: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
@@ -88,7 +88,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
it.each(
[["verified", true, true], ["verified", true, false],
- ["warning", false, true], ["warning", false, false]],
+ ["warning", false, true], ["warning", false, false]],
)("0 others: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
@@ -101,7 +101,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
it.each(
[["verified", true, true], ["verified", true, false],
- ["verified", false, true], ["verified", false, false]],
+ ["verified", false, true], ["verified", false, false]],
)("1 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
@@ -114,7 +114,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
it.each(
[["normal", true, true], ["normal", true, false],
- ["normal", false, true], ["normal", false, false]],
+ ["normal", false, true], ["normal", false, false]],
)("1 unverified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts
index cf9a5f0089..277260bf29 100644
--- a/test/utils/arrays-test.ts
+++ b/test/utils/arrays-test.ts
@@ -29,7 +29,6 @@ import {
ArrayUtil,
GroupedArray,
} from "../../src/utils/arrays";
-import { objectFromEntries } from "../../src/utils/objects";
function expectSample(i: number, input: number[], expected: number[], smooth = false) {
console.log(`Resample case index: ${i}`); // for debugging test failures
@@ -336,7 +335,7 @@ describe('arrays', () => {
expect(result).toBeDefined();
expect(result.value).toBeDefined();
- const asObject = objectFromEntries(result.value.entries());
+ const asObject = Object.fromEntries(result.value.entries());
expect(asObject).toMatchObject(output);
});
});
diff --git a/test/utils/objects-test.ts b/test/utils/objects-test.ts
index 154fa3604f..b360fbd1d1 100644
--- a/test/utils/objects-test.ts
+++ b/test/utils/objects-test.ts
@@ -18,7 +18,6 @@ import {
objectClone,
objectDiff,
objectExcluding,
- objectFromEntries,
objectHasDiff,
objectKeyChanges,
objectShallowClone,
@@ -242,21 +241,4 @@ describe('objects', () => {
expect(result.test.third).not.toBe(a.test.third);
});
});
-
- describe('objectFromEntries', () => {
- it('should create an object from an array of entries', () => {
- const output = { a: 1, b: 2, c: 3 };
- const result = objectFromEntries(Object.entries(output));
- expect(result).toBeDefined();
- expect(result).toMatchObject(output);
- });
-
- it('should maintain pointers in values', () => {
- const output = { a: {}, b: 2, c: 3 };
- const result = objectFromEntries(Object.entries(output));
- expect(result).toBeDefined();
- expect(result).toMatchObject(output);
- expect(result['a']).toBe(output.a);
- });
- });
});
diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts
index af92987a3d..8bc602fe35 100644
--- a/test/utils/test-utils.ts
+++ b/test/utils/test-utils.ts
@@ -15,7 +15,13 @@ limitations under the License.
*/
import { MatrixClient } from "matrix-js-sdk/src/client";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
+import { mkEvent, mkStubRoom } from "../test-utils";
+import { EnhancedMap } from "../../src/utils/maps";
+import { EventEmitter } from "events";
// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent
// ready state without needing to wire up a dispatcher and pretend to be a js-sdk client.
@@ -31,3 +37,48 @@ export const resetAsyncStoreWithClient = async (store: AsyncStoreWithClient
// @ts-ignore
await store.onNotReady();
};
+
+export const mockStateEventImplementation = (events: MatrixEvent[]) => {
+ const stateMap = new EnhancedMap>();
+ events.forEach(event => {
+ stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event);
+ });
+
+ return (eventType: string, stateKey?: string) => {
+ if (stateKey || stateKey === "") {
+ return stateMap.get(eventType)?.get(stateKey) || null;
+ }
+ return Array.from(stateMap.get(eventType)?.values() || []);
+ };
+};
+
+export const mkRoom = (client: MatrixClient, roomId: string, rooms?: ReturnType[]) => {
+ const room = mkStubRoom(roomId, roomId, client);
+ room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([]));
+ rooms?.push(room);
+ return room;
+};
+
+export const mkSpace = (
+ client: MatrixClient,
+ spaceId: string,
+ rooms?: ReturnType[],
+ children: string[] = [],
+) => {
+ const space = mkRoom(client, spaceId, rooms);
+ space.isSpaceRoom.mockReturnValue(true);
+ space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId =>
+ mkEvent({
+ event: true,
+ type: EventType.SpaceChild,
+ room: spaceId,
+ user: "@user:server",
+ skey: roomId,
+ content: { via: [] },
+ ts: Date.now(),
+ }),
+ )));
+ return space;
+};
+
+export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r));
diff --git a/yarn.lock b/yarn.lock
index 5283f5778a..f121112a1b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3233,9 +3233,9 @@ eslint-config-google@^0.14.0:
resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
-"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
- version "0.3.2"
- resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/8529f1d77863db6327cf1a1a4fa65d06cc26f91b"
+"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945":
+ version "0.3.5"
+ resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/2306b3d4da4eba908b256014b979f1d3d43d2945"
eslint-plugin-react-hooks@^4.2.0:
version "4.2.0"
@@ -5445,10 +5445,10 @@ mathml-tag-names@^2.1.3:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
-matrix-js-sdk@12.1.0:
- version "12.1.0"
- resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.1.0.tgz#7d159dd9bc03701e45a6b2777f1fa582a7e8b970"
- integrity sha512-/fSqOjD+mTlMD+/B3s3Ja6BfI46FnTDl43ojzGDUOsHRRmSYUmoONb83qkH5Fjm8cI2q5ZBJMsBfjuZwLVeiZw==
+matrix-js-sdk@12.2.0:
+ version "12.2.0"
+ resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.2.0.tgz#e1dc7ddac054289cb24ee3d11dba8a5ba5ddecf5"
+ integrity sha512-foSs3uKRc6uvFNhgY35eErBvLWVDd5RNIxxsdFKlmU3B+70YUf3BP3petyBNW34ORyOqNdX36IiApfLo3npNEw==
dependencies:
"@babel/runtime" "^7.12.5"
another-json "^0.2.0"