Merge branch 'develop' of git+ssh://github.com/matrix-org/matrix-react-sdk into develop

This commit is contained in:
Matthew Hodgson 2021-03-08 04:57:10 +00:00
commit c02d03cc5b
146 changed files with 8365 additions and 1072 deletions

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ package-lock.json
/src/component-index.js
.DS_Store
*.tmp

View file

@ -1,3 +1,81 @@
Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0)
## Security notice
matrix-react-sdk 3.15.0 fixes a low severity issue (CVE-2021-21320) where the
user content sandbox can be abused to trick users into opening unexpected
documents. The content is opened with a `blob` origin that cannot access Matrix
user data, so messages and secrets are not at risk. Thanks to @keerok for
responsibly disclosing this via Matrix's Security Disclosure Policy.
## All changes
* Upgrade to JS SDK 9.8.0
Changes in [3.15.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0-rc.1) (2021-02-24)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0...v3.15.0-rc.1)
* Upgrade to JS SDK 9.8.0-rc.1
* Translations update from Weblate
[\#5683](https://github.com/matrix-org/matrix-react-sdk/pull/5683)
* Fix object diffing when objects have different keys
[\#5681](https://github.com/matrix-org/matrix-react-sdk/pull/5681)
* Add <code> if it's missing
[\#5673](https://github.com/matrix-org/matrix-react-sdk/pull/5673)
* Add email only if the verification is complete
[\#5629](https://github.com/matrix-org/matrix-react-sdk/pull/5629)
* Fix portrait videocalls
[\#5676](https://github.com/matrix-org/matrix-react-sdk/pull/5676)
* Tweak code block icon positions
[\#5643](https://github.com/matrix-org/matrix-react-sdk/pull/5643)
* Revert "Improve URL preview formatting and image upload thumbnail size"
[\#5677](https://github.com/matrix-org/matrix-react-sdk/pull/5677)
* Fix context menu leaving visible area
[\#5644](https://github.com/matrix-org/matrix-react-sdk/pull/5644)
* Jitsi conferences names, take 3
[\#5675](https://github.com/matrix-org/matrix-react-sdk/pull/5675)
* Update isUserOnDarkTheme to take use_system_theme in account
[\#5670](https://github.com/matrix-org/matrix-react-sdk/pull/5670)
* Discard some dead code
[\#5665](https://github.com/matrix-org/matrix-react-sdk/pull/5665)
* Add developer tool to explore and edit settings
[\#5664](https://github.com/matrix-org/matrix-react-sdk/pull/5664)
* Use and create new room helpers
[\#5663](https://github.com/matrix-org/matrix-react-sdk/pull/5663)
* Clear message previews when the maximum limit is reached for history
[\#5661](https://github.com/matrix-org/matrix-react-sdk/pull/5661)
* VoIP virtual rooms, mk II
[\#5639](https://github.com/matrix-org/matrix-react-sdk/pull/5639)
* Disable chat effects when reduced motion preferred
[\#5660](https://github.com/matrix-org/matrix-react-sdk/pull/5660)
* Improve URL preview formatting and image upload thumbnail size
[\#5637](https://github.com/matrix-org/matrix-react-sdk/pull/5637)
* Fix border radius when the panel is collapsed
[\#5641](https://github.com/matrix-org/matrix-react-sdk/pull/5641)
* Use a more generic layout setting - useIRCLayout → layout
[\#5571](https://github.com/matrix-org/matrix-react-sdk/pull/5571)
* Remove redundant lockOrigin parameter from usercontent
[\#5657](https://github.com/matrix-org/matrix-react-sdk/pull/5657)
* Set ICE candidate pool size option
[\#5655](https://github.com/matrix-org/matrix-react-sdk/pull/5655)
* Prepare to encrypt when a call arrives
[\#5654](https://github.com/matrix-org/matrix-react-sdk/pull/5654)
* Use config for host signup branding
[\#5650](https://github.com/matrix-org/matrix-react-sdk/pull/5650)
* Use randomly generated conference names for Jitsi
[\#5649](https://github.com/matrix-org/matrix-react-sdk/pull/5649)
* Modified regex to account for an immediate new line after slash commands
[\#5647](https://github.com/matrix-org/matrix-react-sdk/pull/5647)
* Fix codeblock scrollbar color for non-Firefox
[\#5642](https://github.com/matrix-org/matrix-react-sdk/pull/5642)
* Fix codeblock scrollbar colors
[\#5630](https://github.com/matrix-org/matrix-react-sdk/pull/5630)
* Added loading and disabled the button while searching for server
[\#5634](https://github.com/matrix-org/matrix-react-sdk/pull/5634)
Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0)

View file

@ -3,12 +3,15 @@ module.exports = {
"presets": [
["@babel/preset-env", {
"targets": [
"last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions"
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 Edge versions",
],
}],
"@babel/preset-typescript",
"@babel/preset-flow",
"@babel/preset-react"
"@babel/preset-react",
],
"plugins": [
["@babel/plugin-proposal-decorators", {legacy: true}],
@ -18,6 +21,6 @@ module.exports = {
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-transform-flow-comments",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime"
]
"@babel/plugin-transform-runtime",
],
};

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "3.14.0",
"version": "3.15.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -101,7 +101,7 @@
"tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0",
"velocity-animate": "^1.5.2",
"velocity-animate": "^2.0.6",
"what-input": "^5.2.10",
"zxcvbn": "^4.4.2"
},

View file

@ -27,6 +27,9 @@
@import "./structures/_RoomView.scss";
@import "./structures/_ScrollPanel.scss";
@import "./structures/_SearchBox.scss";
@import "./structures/_SpacePanel.scss";
@import "./structures/_SpaceRoomDirectory.scss";
@import "./structures/_SpaceRoomView.scss";
@import "./structures/_TabbedView.scss";
@import "./structures/_ToastContainer.scss";
@import "./structures/_UploadBar.scss";
@ -56,6 +59,7 @@
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_StatusMessageContextMenu.scss";
@import "./views/context_menus/_TagTileContextMenu.scss";
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@ -89,6 +93,7 @@
@import "./views/dialogs/_SettingsDialog.scss";
@import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
@import "./views/dialogs/_SpaceSettingsDialog.scss";
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss";
@ -212,6 +217,7 @@
@import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_E2eAdvancedPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_SpellCheckLanguages.scss";
@import "./views/settings/_IntegrationManager.scss";
@import "./views/settings/_Notifications.scss";
@import "./views/settings/_PhoneNumbers.scss";
@ -232,6 +238,9 @@
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
@import "./views/spaces/_SpaceBasicSettings.scss";
@import "./views/spaces/_SpaceCreateMenu.scss";
@import "./views/spaces/_SpacePublicShare.scss";
@import "./views/terms/_InlineTermsAgreement.scss";
@import "./views/toasts/_AnalyticsToast.scss";
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
$roomListCollapsedWidth: 68px;
.mx_LeftPanel {
background-color: $roomlist-bg-color;
@ -37,18 +38,12 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
// GroupFilterPanel handles its own CSS
}
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
.mx_LeftPanel_roomListContainer {
width: 100%;
}
}
// Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel_roomListContainer {
width: calc(100% - $groupFilterPanelWidth);
background-color: $roomlist-bg-color;
flex: 1 0 0;
min-width: 0;
// Create another flexbox (this time a column) for the room list components
display: flex;
flex-direction: column;
@ -168,17 +163,10 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
// These styles override the defaults for the minimized (66px) layout
&.mx_LeftPanel_minimized {
min-width: unset;
// We have to forcefully set the width to override the resizer's style attribute.
&.mx_LeftPanel_hasGroupFilterPanel {
width: calc(68px + $groupFilterPanelWidth) !important;
}
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
width: 68px !important;
}
width: unset !important;
.mx_LeftPanel_roomListContainer {
width: 68px;
width: $roomListCollapsedWidth;
.mx_LeftPanel_userHeader {
flex-direction: row;

View file

@ -18,6 +18,7 @@ limitations under the License.
display: flex;
flex-direction: row;
min-width: 0;
min-height: 0;
height: 100%;
}

View file

@ -160,3 +160,20 @@ limitations under the License.
mask-position: center;
}
}
.mx_RightPanel_scopeHeader {
margin: 24px;
text-align: center;
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
.mx_BaseAvatar {
margin-right: 8px;
vertical-align: middle;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
}

View file

@ -20,35 +20,54 @@ limitations under the License.
flex-direction: column;
}
@keyframes mx_RoomView_fileDropTarget_animation {
from {
opacity: 0;
}
to {
opacity: 0.95;
}
}
.mx_RoomView_fileDropTarget {
min-width: 0px;
width: 100%;
height: 100%;
font-size: $font-18px;
text-align: center;
pointer-events: none;
padding-left: 12px;
padding-right: 12px;
margin-left: -12px;
background-color: $primary-bg-color;
opacity: 0.95;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background-color: $droptarget-bg-color;
border: 2px #e1dddd solid;
border-bottom: none;
position: absolute;
top: 52px;
bottom: 0px;
z-index: 3000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
animation: mx_RoomView_fileDropTarget_animation;
animation-duration: 0.5s;
}
.mx_RoomView_fileDropTargetLabel {
top: 50%;
width: 100%;
margin-top: -50px;
position: absolute;
@keyframes mx_RoomView_fileDropTarget_image_animation {
from {
width: 0px;
}
to {
width: 32px;
}
}
.mx_RoomView_fileDropTarget_image {
animation: mx_RoomView_fileDropTarget_image_animation;
animation-duration: 0.5s;
margin-bottom: 16px;
}
.mx_RoomView_auxPanel {
@ -117,7 +136,6 @@ limitations under the License.
}
.mx_RoomView_body {
position: relative; //for .mx_RoomView_auxPanel_fullHeight
display: flex;
flex-direction: column;
flex: 1;

View file

@ -0,0 +1,349 @@
/*
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.
*/
$topLevelHeight: 32px;
$nestedHeight: 24px;
$gutterSize: 17px;
$activeStripeSize: 4px;
$activeBorderTransparentGap: 2px;
$activeBackgroundColor: $roomtile-selected-bg-color;
$activeBorderColor: $secondary-fg-color;
.mx_SpacePanel {
flex: 0 0 auto;
background-color: $groupFilterPanel-bg-color;
padding: 0;
margin: 0;
// Create another flexbox so the Panel fills the container
display: flex;
flex-direction: column;
overflow-y: auto;
.mx_SpacePanel_spaceTreeWrapper {
flex: 1;
}
.mx_SpacePanel_toggleCollapse {
flex: 0 0 auto;
width: 40px;
height: 40px;
mask-position: center;
mask-size: 32px;
mask-repeat: no-repeat;
margin-left: $gutterSize;
margin-bottom: 12px;
background-color: $roomlist-header-color;
mask-image: url('$(res)/img/element-icons/expand-space-panel.svg');
&.expanded {
transform: scaleX(-1);
}
}
ul {
margin: 0;
list-style: none;
padding: 0;
padding-left: 16px;
}
.mx_AutoHideScrollbar {
padding: 16px 12px 16px 0;
}
.mx_SpaceButton_toggleCollapse {
cursor: pointer;
}
.mx_SpaceItem.collapsed {
.mx_SpaceButton {
.mx_NotificationBadge {
right: -4px;
top: -4px;
}
}
& > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse {
transform: rotate(-90deg);
}
& > .mx_SpaceTreeLevel {
display: none;
}
}
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
margin-left: $gutterSize;
}
.mx_SpaceButton {
border-radius: 8px;
position: relative;
margin-bottom: 2px;
display: flex;
align-items: center;
padding: 4px;
&.mx_SpaceButton_active {
&:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper {
background-color: $activeBackgroundColor;
border-radius: 8px;
}
&.mx_SpaceButton_narrow {
.mx_BaseAvatar, .mx_SpaceButton_avatarPlaceholder {
border: 2px $activeBorderColor solid;
border-radius: 11px;
}
}
}
.mx_SpaceButton_selectionWrapper {
display: flex;
flex: 1;
align-items: center;
}
.mx_SpaceButton_name {
flex: 1;
margin-left: 8px;
white-space: nowrap;
display: block;
max-width: 150px;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 8px;
font-size: $font-14px;
line-height: $font-18px;
}
.mx_SpaceButton_toggleCollapse {
width: calc($gutterSize - $activeStripeSize);
margin-left: 1px;
height: 20px;
mask-position: center;
mask-size: 20px;
mask-repeat: no-repeat;
background-color: $roomlist-header-color;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
.mx_SpaceButton_icon {
width: $topLevelHeight;
min-width: $topLevelHeight;
height: $topLevelHeight;
border-radius: 8px;
position: relative;
&::before {
position: absolute;
content: '';
width: $topLevelHeight;
height: $topLevelHeight;
top: 0;
left: 0;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 18px;
}
}
&.mx_SpaceButton_home .mx_SpaceButton_icon {
background-color: #ffffff;
&::before {
background-color: #3f3d3d;
mask-image: url('$(res)/img/element-icons/home.svg');
}
}
.mx_SpaceButton_avatarPlaceholder {
border: $activeBorderTransparentGap transparent solid;
padding: $activeBorderTransparentGap;
}
&.mx_SpaceButton_new .mx_SpaceButton_icon {
background-color: $accent-color;
transition: all .1s ease-in-out; // TODO transition
&::before {
background-color: #ffffff;
mask-image: url('$(res)/img/element-icons/plus.svg');
transition: all .2s ease-in-out; // TODO transition
}
}
&.mx_SpaceButton_newCancel .mx_SpaceButton_icon {
background-color: $icon-button-color;
&::before {
transform: rotate(45deg);
}
}
.mx_BaseAvatar {
/* moving the border-radius to this element from _image
element so we can add a border to it without the initials being displaced */
overflow: hidden;
border: 2px transparent solid;
padding: $activeBorderTransparentGap;
.mx_BaseAvatar_initial {
top: $activeBorderTransparentGap;
left: $activeBorderTransparentGap;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
}
.mx_SpaceButton_menuButton {
width: 20px;
min-width: 20px; // yay flex
height: 20px;
margin-top: auto;
margin-bottom: auto;
position: relative;
display: none;
&::before {
top: 2px;
left: 2px;
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/context-menu.svg');
background: $primary-fg-color;
}
}
}
.mx_SpacePanel_badgeContainer {
height: 16px;
// don't set width so that it takes no space when there is no badge to show
margin: auto 0; // vertically align
// Create a flexbox to make aligning dot badges easier
display: flex;
align-items: center;
.mx_NotificationBadge {
margin: 0 2px; // centering
}
.mx_NotificationBadge_dot {
// make the smaller dot occupy the same width for centering
margin-left: 7px;
margin-right: 7px;
}
}
&.collapsed {
.mx_SpaceButton {
.mx_SpacePanel_badgeContainer {
position: absolute;
right: 0px;
top: 2px;
}
}
}
&:not(.collapsed) {
.mx_SpaceButton:hover,
.mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen {
// Hide the badge container on hover because it'll be a menu button
.mx_SpacePanel_badgeContainer {
width: 0;
height: 0;
display: none;
}
.mx_SpaceButton_menuButton {
display: block;
}
}
}
/* root space buttons are bigger and not indented */
& > .mx_AutoHideScrollbar {
& > .mx_SpaceButton {
height: $topLevelHeight;
&.mx_SpaceButton_active::before {
height: $topLevelHeight;
}
}
& > ul {
padding-left: 0;
}
}
}
.mx_SpacePanel_contextMenu {
.mx_SpacePanel_contextMenu_header {
margin: 12px 16px 12px;
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
}
.mx_IconizedContextMenu_optionList .mx_AccessibleButton.mx_SpacePanel_contextMenu_inviteButton {
color: $accent-color;
.mx_SpacePanel_iconInvite::before {
background-color: $accent-color;
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
.mx_SpacePanel_iconSettings::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
.mx_SpacePanel_iconLeave::before {
mask-image: url('$(res)/img/element-icons/leave.svg');
}
.mx_SpacePanel_iconHome::before {
mask-image: url('$(res)/img/element-icons/roomlist/home.svg');
}
.mx_SpacePanel_iconMembers::before {
mask-image: url('$(res)/img/element-icons/room/members.svg');
}
.mx_SpacePanel_iconPlus::before {
mask-image: url('$(res)/img/element-icons/plus.svg');
}
.mx_SpacePanel_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
}
.mx_SpacePanel_sharePublicSpace {
margin: 0;
}

View file

@ -0,0 +1,231 @@
/*
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.
*/
.mx_SpaceRoomDirectory_dialogWrapper > .mx_Dialog {
max-width: 960px;
height: 100%;
}
.mx_SpaceRoomDirectory {
height: 100%;
margin-bottom: 12px;
color: $primary-fg-color;
word-break: break-word;
display: flex;
flex-direction: column;
.mx_Dialog_title {
display: flex;
.mx_BaseAvatar {
margin-right: 16px;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
> div {
> h1 {
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
margin: 0;
}
> div {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
}
}
}
.mx_Dialog_content {
// TODO fix scrollbar
//display: flex;
//flex-direction: column;
//height: calc(100% - 80px);
.mx_AccessibleButton_kind_link {
padding: 0;
}
.mx_SearchBox {
margin: 24px 0 28px;
}
.mx_SpaceRoomDirectory_listHeader {
display: flex;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
.mx_FormButton {
margin-bottom: 8px;
}
> span {
margin: auto 0 0 auto;
}
}
}
}
.mx_SpaceRoomDirectory_list {
margin-top: 8px;
.mx_SpaceRoomDirectory_roomCount {
> h3 {
display: inline;
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
color: $primary-fg-color;
}
> span {
margin-left: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $secondary-fg-color;
}
}
.mx_SpaceRoomDirectory_subspace {
margin-top: 8px;
.mx_SpaceRoomDirectory_subspace_info {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
color: $secondary-fg-color;
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
.mx_BaseAvatar {
margin-right: 12px;
vertical-align: middle;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
.mx_SpaceRoomDirectory_actions {
text-align: right;
height: min-content;
margin-left: auto;
margin-right: 16px;
}
}
.mx_SpaceRoomDirectory_subspace_children {
margin-left: 12px;
border-left: 2px solid $space-button-outline-color;
padding-left: 24px;
}
}
.mx_SpaceRoomDirectory_roomTile {
padding: 16px;
border-radius: 8px;
border: 1px solid $space-button-outline-color;
margin: 8px 0 16px;
display: flex;
min-height: 76px;
box-sizing: border-box;
&.mx_AccessibleButton:hover {
background-color: rgba(141, 151, 165, 0.1);
}
.mx_BaseAvatar {
margin-right: 16px;
margin-top: 6px;
}
.mx_SpaceRoomDirectory_roomTile_info {
display: inline-block;
font-size: $font-15px;
flex-grow: 1;
height: min-content;
margin: auto 0;
.mx_SpaceRoomDirectory_roomTile_name {
font-weight: $font-semi-bold;
line-height: $font-18px;
}
.mx_SpaceRoomDirectory_roomTile_topic {
line-height: $font-24px;
color: $secondary-fg-color;
}
}
.mx_SpaceRoomDirectory_roomTile_memberCount {
position: relative;
margin: auto 0 auto 24px;
padding: 0 0 0 28px;
line-height: $font-24px;
display: inline-block;
width: 32px;
&::before {
position: absolute;
content: '';
width: 24px;
height: 24px;
top: 0;
left: 0;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
background-color: $secondary-fg-color;
mask-image: url('$(res)/img/element-icons/community-members.svg');
}
}
.mx_SpaceRoomDirectory_actions {
width: 180px;
text-align: right;
height: min-content;
margin: auto 0 auto 28px;
.mx_AccessibleButton {
vertical-align: middle;
& + .mx_AccessibleButton {
margin-left: 24px;
}
}
}
}
.mx_SpaceRoomDirectory_actions {
.mx_SpaceRoomDirectory_actionsText {
font-weight: normal;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
}
.mx_Checkbox {
display: inline-block;
}
}
}

View file

@ -0,0 +1,336 @@
/*
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.
*/
$SpaceRoomViewInnerWidth: 428px;
.mx_SpaceRoomView {
.mx_MainSplit > div:first-child {
padding: 80px 60px;
flex-grow: 1;
h1 {
margin: 0;
font-size: $font-24px;
font-weight: $font-semi-bold;
color: $primary-fg-color;
width: max-content;
}
.mx_SpaceRoomView_description {
font-size: $font-15px;
color: $secondary-fg-color;
margin-top: 12px;
margin-bottom: 24px;
}
.mx_SpaceRoomView_buttons {
display: block;
margin-top: 44px;
width: $SpaceRoomViewInnerWidth;
text-align: right; // button alignment right
.mx_FormButton {
padding: 8px 22px;
margin-left: 16px;
}
}
.mx_Field {
max-width: $SpaceRoomViewInnerWidth;
& + .mx_Field {
margin-top: 28px;
}
}
.mx_SpaceRoomView_errorText {
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
color: $notice-primary-color;
margin-bottom: 28px;
}
.mx_AccessibleButton_disabled {
cursor: not-allowed;
}
}
.mx_SpaceRoomView_landing {
overflow-y: auto;
> .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px;
}
.mx_SpaceRoomView_landing_name {
margin: 24px 0 16px;
font-size: $font-15px;
color: $secondary-fg-color;
> span {
display: inline-block;
}
.mx_SpaceRoomView_landing_nameRow {
margin-top: 12px;
> h1 {
display: inline-block;
}
}
.mx_SpaceRoomView_landing_inviter {
.mx_BaseAvatar {
margin-right: 4px;
vertical-align: middle;
}
}
.mx_SpaceRoomView_landing_memberCount {
position: relative;
margin-left: 24px;
padding: 0 0 0 28px;
line-height: $font-24px;
vertical-align: text-bottom;
&::before {
position: absolute;
content: '';
width: 24px;
height: 24px;
top: 0;
left: 0;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
background-color: $accent-color;
mask-image: url('$(res)/img/element-icons/community-members.svg');
}
}
}
.mx_SpaceRoomView_landing_topic {
font-size: $font-15px;
}
.mx_SpaceRoomView_landing_joinButtons {
margin-top: 24px;
.mx_FormButton {
padding: 8px 22px;
}
}
.mx_SpaceRoomView_landing_adminButtons {
margin-top: 32px;
.mx_AccessibleButton {
position: relative;
width: 160px;
height: 124px;
box-sizing: border-box;
padding: 72px 16px 0;
border-radius: 12px;
border: 1px solid $space-button-outline-color;
margin-right: 28px;
margin-bottom: 28px;
font-size: $font-14px;
display: inline-block;
vertical-align: bottom;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: rgba(141, 151, 165, 0.1);
}
&::before, &::after {
position: absolute;
content: "";
left: 16px;
top: 16px;
height: 40px;
width: 40px;
border-radius: 20px;
}
&::after {
mask-position: center;
mask-size: 30px;
mask-repeat: no-repeat;
background: #ffffff; // white icon fill
}
&.mx_SpaceRoomView_landing_inviteButton {
&::before {
background-color: $accent-color;
}
&::after {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
&.mx_SpaceRoomView_landing_addButton {
&::before {
background-color: #ac3ba8;
}
&::after {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
}
&.mx_SpaceRoomView_landing_createButton {
&::before {
background-color: #368bd6;
}
&::after {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
}
&.mx_SpaceRoomView_landing_settingsButton {
&::before {
background-color: #5c56f5;
}
&::after {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
}
}
}
.mx_SpaceRoomDirectory_list {
max-width: 600px;
.mx_SpaceRoomDirectory_roomTile_actions {
display: none;
}
}
}
.mx_SpaceRoomView_privateScope {
.mx_RadioButton {
width: $SpaceRoomViewInnerWidth;
border-radius: 8px;
border: 1px solid $space-button-outline-color;
padding: 16px 16px 16px 72px;
margin-top: 36px;
cursor: pointer;
box-sizing: border-box;
position: relative;
> div:first-of-type {
// hide radio dot
display: none;
}
.mx_RadioButton_content {
margin: 0;
> h3 {
margin: 0 0 4px;
font-size: $font-15px;
font-weight: $font-semi-bold;
line-height: $font-18px;
}
> div {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
}
}
&::before {
content: "";
position: absolute;
height: 32px;
width: 32px;
top: 24px;
left: 20px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
}
.mx_RadioButton_checked {
border-color: $accent-color;
.mx_RadioButton_content {
> div {
color: $primary-fg-color;
}
}
&::before {
background-color: $accent-color;
}
}
.mx_SpaceRoomView_privateScope_justMeButton::before {
mask-image: url('$(res)/img/element-icons/room/members.svg');
}
.mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before {
mask-image: url('$(res)/img/element-icons/community-members.svg');
}
}
.mx_SpaceRoomView_inviteTeammates {
.mx_SpaceRoomView_inviteTeammates_buttons {
color: $secondary-fg-color;
margin-top: 28px;
.mx_AccessibleButton {
position: relative;
display: inline-block;
padding-left: 32px;
line-height: 24px; // to center icons
&::before {
content: "";
position: absolute;
height: 24px;
width: 24px;
top: 0;
left: 0;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
& + .mx_AccessibleButton {
margin-left: 32px;
}
}
.mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
}
}

View file

@ -72,6 +72,7 @@ limitations under the License.
position: relative; // to make default avatars work
margin-right: 8px;
height: 32px; // to remove the unknown 4px gap the browser puts below it
padding: 3px 0; // to align with and without using doubleName
.mx_UserMenu_userAvatar {
border-radius: 32px; // should match avatar size

View file

@ -75,6 +75,11 @@ limitations under the License.
background-color: $menu-selected-color;
}
&.mx_AccessibleButton_disabled {
opacity: 0.5;
cursor: not-allowed;
}
img, .mx_IconizedContextMenu_icon { // icons
width: 16px;
min-width: 16px;

View file

@ -0,0 +1,185 @@
/*
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.
*/
.mx_AddExistingToSpaceDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
}
}
.mx_AddExistingToSpaceDialog {
width: 480px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
.mx_Dialog_title {
display: flex;
.mx_BaseAvatar {
display: inline-flex;
margin: 5px 16px 5px 5px;
vertical-align: middle;
}
.mx_BaseAvatar_image {
border-radius: 8px;
margin: 0;
vertical-align: unset;
}
> div {
> h1 {
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
margin: 0;
}
.mx_AddExistingToSpaceDialog_onlySpace {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
}
}
.mx_Dropdown_input {
border: none;
> .mx_Dropdown_option {
padding-left: 0;
flex: unset;
height: unset;
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
.mx_BaseAvatar {
display: none;
}
}
.mx_Dropdown_menu {
.mx_AddExistingToSpaceDialog_dropdownOptionActive {
color: $accent-color;
padding-right: 32px;
position: relative;
&::before {
content: '';
width: 20px;
height: 20px;
top: 8px;
right: 0;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background-color: $accent-color;
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
}
}
}
}
}
.mx_SearchBox {
margin: 0;
}
.mx_AddExistingToSpaceDialog_errorText {
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
color: $notice-primary-color;
margin-bottom: 28px;
}
.mx_AddExistingToSpaceDialog_content {
.mx_AddExistingToSpaceDialog_noResults {
margin-top: 24px;
}
}
.mx_AddExistingToSpaceDialog_section {
margin-top: 24px;
> h3 {
margin: 0;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
.mx_AddExistingToSpaceDialog_entry {
display: flex;
margin-top: 12px;
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_AddExistingToSpaceDialog_entry_name {
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
}
.mx_FormButton {
min-width: 92px;
font-weight: normal;
box-sizing: border-box;
}
}
}
.mx_AddExistingToSpaceDialog_section_spaces {
.mx_BaseAvatar_image {
border-radius: 8px;
}
}
.mx_AddExistingToSpaceDialog_footer {
display: flex;
margin-top: 32px;
> span {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
> * {
vertical-align: middle;
}
}
.mx_AccessibleButton {
display: inline-block;
}
.mx_AccessibleButton_kind_link {
padding: 0;
}
}
.mx_FormButton {
padding: 8px 22px;
}
}

View file

@ -19,6 +19,11 @@ limitations under the License.
max-width: 580px;
height: 80vh;
max-height: 600px;
// Ensure dialog borders are always white as the HostSignupDialog
// does not yet support dark mode or theming in general.
// In the future we might want to pass the theme to the called
// iframe, should some hosting provider have that need.
background-color: #ffffff;
.mx_HostSignupDialog_info {
text-align: center;

View file

@ -0,0 +1,55 @@
/*
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.
*/
.mx_SpaceSettingsDialog {
width: 480px;
color: $primary-fg-color;
.mx_SpaceSettings_errorText {
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
color: $notice-primary-color;
margin-bottom: 28px;
}
.mx_ToggleSwitch {
display: inline-block;
vertical-align: middle;
margin-left: 16px;
}
.mx_AccessibleButton_kind_danger {
margin-top: 28px;
}
.mx_SpaceSettingsDialog_buttons {
display: flex;
margin-top: 64px;
.mx_AccessibleButton {
display: inline-block;
}
.mx_AccessibleButton_kind_link {
margin-left: auto;
}
}
.mx_FormButton {
padding: 8px 22px;
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
.m_RoomView_auxPanel_stateViews {
padding: 5px;
padding-left: 19px;
border-bottom: 1px solid #e5e5e5;
border-bottom: 1px solid $primary-hairline-color;
}
.m_RoomView_auxPanel_stateViews_span a {

View file

@ -257,17 +257,13 @@ $left-gutter: 64px;
display: inline-block;
width: 14px;
height: 14px;
top: 29px;
// This aligns the avatar with the last line of the
// message. We want to move it one line up - 2.2rem
top: -2.2rem;
user-select: none;
z-index: 1;
}
.mx_EventTile_continuation .mx_EventTile_readAvatars,
.mx_EventTile_info .mx_EventTile_readAvatars,
.mx_EventTile_emote .mx_EventTile_readAvatars {
top: 7px;
}
.mx_EventTile_readAvatars .mx_BaseAvatar {
position: absolute;
display: inline-block;

View file

@ -105,16 +105,9 @@ $left-gutter: 64px;
}
.mx_EventTile_readAvatars {
top: 27px;
}
&.mx_EventTile_continuation .mx_EventTile_readAvatars,
&.mx_EventTile_emote .mx_EventTile_readAvatars {
top: 5px;
}
&.mx_EventTile_info .mx_EventTile_readAvatars {
top: 4px;
// This aligns the avatar with the last line of the
// message. We want to move it one line up - 2rem
top: -2rem;
}
.mx_EventTile_content .markdown-body {

View file

@ -19,6 +19,7 @@ limitations under the License.
flex-direction: column;
flex: 1;
overflow-y: auto;
margin-top: 8px;
}
.mx_MemberInfo_name {

View file

@ -44,6 +44,12 @@ limitations under the License.
.mx_AutoHideScrollbar {
flex: 1 1 0;
}
.mx_RightPanel_scopeHeader {
// vertically align with position on other right panel cards
// to prevent it bouncing as user navigates right panel
margin-top: -8px;
}
}
.mx_GroupMemberList_query,

View file

@ -227,18 +227,6 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
}
.mx_MessageComposer_hangup::before {
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
}
.mx_MessageComposer_voicecall::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
.mx_MessageComposer_videocall::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
.mx_MessageComposer_emoji::before {
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
}
@ -247,6 +235,32 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
}
.mx_MessageComposer_sendMessage {
cursor: pointer;
position: relative;
margin-right: 6px;
width: 32px;
height: 32px;
border-radius: 100%;
background-color: $button-bg-color;
&::before {
position: absolute;
height: 16px;
width: 16px;
top: 8px;
left: 9px;
mask-image: url('$(res)/img/element-icons/send-message.svg');
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
background-color: $button-fg-color;
content: '';
}
}
.mx_MessageComposer_formatting {
cursor: pointer;
margin: 0 11px;

View file

@ -252,6 +252,19 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
}
.mx_RoomHeader_voiceCallButton::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
// The call button SVG is padded slightly differently, so match it up to the size
// of the other icons
mask-size: 20px;
mask-position: center;
}
.mx_RoomHeader_videoCallButton::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
.mx_RoomHeader_showPanel {
height: 16px;
}

View file

@ -19,7 +19,10 @@ limitations under the License.
}
.mx_RoomList_iconPlus::before {
mask-image: url('$(res)/img/element-icons/roomlist/plus.svg');
mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
}
.mx_RoomList_iconHash::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
}
.mx_RoomList_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');

View file

@ -0,0 +1,35 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ExistingSpellCheckLanguage {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.mx_ExistingSpellCheckLanguage_language {
flex: 1;
margin-right: 10px;
}
.mx_GeneralUserSettingsTab_spellCheckLanguageInput {
margin-top: 1em;
margin-bottom: 1em;
}
.mx_SpellCheckLanguages {
@mixin mx_Settings_fullWidthField;
}

View file

@ -0,0 +1,86 @@
/*
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.
*/
.mx_SpaceBasicSettings {
.mx_Field {
margin: 32px 0;
}
.mx_SpaceBasicSettings_avatarContainer {
display: flex;
margin-top: 24px;
.mx_SpaceBasicSettings_avatar {
position: relative;
height: 80px;
width: 80px;
background-color: $tertiary-fg-color;
border-radius: 16px;
}
img.mx_SpaceBasicSettings_avatar {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 16px;
}
// only show it when the button is a div and not an img (has avatar)
div.mx_SpaceBasicSettings_avatar {
cursor: pointer;
&::before {
content: "";
position: absolute;
height: 80px;
width: 80px;
top: 0;
left: 0;
background-color: #ffffff; // white icon fill
mask-repeat: no-repeat;
mask-position: center;
mask-size: 20px;
mask-image: url('$(res)/img/element-icons/camera.svg');
}
}
> input[type="file"] {
display: none;
}
> .mx_AccessibleButton_kind_link {
display: inline-block;
padding: 0;
margin: auto 16px;
color: #368bd6;
}
> .mx_SpaceBasicSettings_avatar_remove {
color: $notice-primary-color;
}
}
.mx_FormButton {
padding: 8px 22px;
margin-left: auto;
display: block;
width: min-content;
}
.mx_AccessibleButton_disabled {
cursor: not-allowed;
}
}

View file

@ -0,0 +1,138 @@
/*
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.
*/
// TODO: the space panel currently does not have a fixed width,
// just the headers at each level have a max-width of 150px
// so this will look slightly off for now. We should probably use css grid for the whole main layout...
$spacePanelWidth: 200px;
.mx_SpaceCreateMenu_wrapper {
// background blur everything except SpacePanel
.mx_ContextualMenu_background {
background-color: $dialog-backdrop-color;
opacity: 0.6;
left: $spacePanelWidth;
}
.mx_ContextualMenu {
padding: 24px;
width: 480px;
box-sizing: border-box;
background-color: $primary-bg-color;
> div {
> h2 {
font-weight: $font-semi-bold;
font-size: $font-18px;
margin-top: 4px;
}
> p {
font-size: $font-15px;
color: $secondary-fg-color;
margin: 0;
}
}
.mx_SpaceCreateMenuType {
position: relative;
padding: 16px 32px 16px 72px;
width: 432px;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid $input-darker-bg-color;
font-size: $font-15px;
margin: 20px 0;
> h3 {
font-weight: $font-semi-bold;
margin: 0 0 4px;
}
> span {
color: $secondary-fg-color;
}
&::before {
position: absolute;
content: '';
width: 32px;
height: 32px;
top: 24px;
left: 20px;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 32px;
background-color: $tertiary-fg-color;
}
&:hover {
border-color: $accent-color;
&::before {
background-color: $accent-color;
}
> span {
color: $primary-fg-color;
}
}
}
.mx_SpaceCreateMenuType_public::before {
mask-image: url('$(res)/img/globe.svg');
mask-size: 26px;
}
.mx_SpaceCreateMenuType_private::before {
mask-image: url('$(res)/img/element-icons/lock.svg');
}
.mx_SpaceCreateMenu_back {
width: 28px;
height: 28px;
position: relative;
background-color: $theme-button-bg-color;
border-radius: 14px;
margin-bottom: 12px;
&::before {
content: "";
position: absolute;
height: 28px;
width: 28px;
top: 0;
left: 0;
background-color: $muted-fg-color;
transform: rotate(90deg);
mask-repeat: no-repeat;
mask-position: 2px 3px;
mask-size: 24px;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
}
.mx_FormButton {
padding: 8px 22px;
margin-left: auto;
display: block;
width: min-content;
}
.mx_AccessibleButton_disabled {
cursor: not-allowed;
}
}
}

View file

@ -0,0 +1,60 @@
/*
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.
*/
.mx_SpacePublicShare {
.mx_AccessibleButton {
border: 1px solid $space-button-outline-color;
box-sizing: border-box;
border-radius: 8px;
padding: 12px 24px 12px 52px;
margin-top: 16px;
width: $SpaceRoomViewInnerWidth;
font-size: $font-15px;
line-height: $font-24px;
position: relative;
display: flex;
> span {
color: #368bd6;
margin-left: auto;
}
&:hover {
background-color: rgba(141, 151, 165, 0.1);
}
&::before {
content: "";
position: absolute;
width: 30px;
height: 30px;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
background: $muted-fg-color;
left: 12px;
top: 9px;
}
&.mx_SpacePublicShare_shareButton::before {
mask-image: url('$(res)/img/element-icons/link.svg');
}
&.mx_SpacePublicShare_inviteButton::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
}

View file

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7701 16.617H22.3721L18.614 20.3751C18.3137 20.6754 18.3137 21.1683 18.614 21.4686C18.9143 21.769 19.3995 21.769 19.6998 21.4686L24.7747 16.3937C25.0751 16.0934 25.0751 15.6082 24.7747 15.3079L19.7075 10.2253C19.4072 9.92492 18.922 9.92492 18.6217 10.2253C18.3214 10.5256 18.3214 11.0107 18.6217 11.3111L22.3721 15.0768H13.7701C13.3465 15.0768 13 15.4234 13 15.8469C13 16.2705 13.3465 16.617 13.7701 16.617Z" fill="#86888A"/>
<rect x="7" y="10" width="1.5" height="12" rx="0.75" fill="#86888A"/>
</svg>

After

Width:  |  Height:  |  Size: 651 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5285 6.54089L13.0273 6.04207C14.4052 4.66426 16.6259 4.65104 17.9874 6.01253C19.349 7.37402 19.3357 9.59466 17.9579 10.9725L15.5878 13.3425C14.21 14.7203 11.9893 14.7335 10.6277 13.372M11.4717 17.4589L10.9727 17.9579C9.59481 19.3357 7.37409 19.349 6.01256 17.9875C4.65102 16.626 4.66426 14.4053 6.04211 13.0275L8.41203 10.6577C9.78988 9.27988 12.0106 9.26665 13.3721 10.6281" stroke="black" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 549 B

View file

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1113 2.6665C11.1839 2.6665 8.00016 5.85026 8.00016 9.77762V13.3332L7.3335 13.3332C6.22893 13.3332 5.3335 14.2286 5.3335 15.3332V27.3332C5.3335 28.4377 6.22893 29.3332 7.3335 29.3332H24.6668C25.7714 29.3332 26.6668 28.4377 26.6668 27.3332V15.3332C26.6668 14.2286 25.7714 13.3332 24.6668 13.3332L24.0002 13.3332V9.77762C24.0002 5.85026 20.8164 2.6665 16.8891 2.6665H15.1113ZM20.4446 13.3332V9.77762C20.4446 7.81394 18.8527 6.22206 16.8891 6.22206H15.1113C13.1476 6.22206 11.5557 7.81394 11.5557 9.77762V13.3332H20.4446Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.74986 3.55554C8.74986 3.14133 8.41408 2.80554 7.99986 2.80554C7.58565 2.80554 7.24986 3.14133 7.24986 3.55554V7.24999L3.55542 7.24999C3.14121 7.24999 2.80542 7.58577 2.80542 7.99999C2.80542 8.4142 3.14121 8.74999 3.55542 8.74999L7.24987 8.74999V12.4444C7.24987 12.8586 7.58565 13.1944 7.99987 13.1944C8.41408 13.1944 8.74987 12.8586 8.74987 12.4444V8.74999L12.4443 8.74999C12.8585 8.74999 13.1943 8.4142 13.1943 7.99999C13.1943 7.58577 12.8585 7.24999 12.4443 7.24999L8.74986 7.24999V3.55554Z" fill="#8E99A4"/>
</svg>

After

Width:  |  Height:  |  Size: 670 B

View file

@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="24" height="24" viewBox="-0.4 1 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.1001 9C18.7779 9 18.5168 8.73883 18.5168 8.41667V6.08333H16.1834C15.8613 6.08333 15.6001 5.82217 15.6001 5.5C15.6001 5.17783 15.8613 4.91667 16.1834 4.91667H18.5168V2.58333C18.5168 2.26117 18.7779 2 19.1001 2C19.4223 2 19.6834 2.26117 19.6834 2.58333V4.91667H22.0168C22.3389 4.91667 22.6001 5.17783 22.6001 5.5C22.6001 5.82217 22.3389 6.08333 22.0168 6.08333H19.6834V8.41667C19.6834 8.73883 19.4223 9 19.1001 9ZM19.6001 11C20.0669 11 20.5212 10.9467 20.9574 10.8458C21.1161 11.5383 21.2 12.2594 21.2 13C21.2 16.1409 19.6917 18.9294 17.3598 20.6808V20.6807C16.0014 21.7011 14.3635 22.3695 12.5815 22.5505C12.2588 22.5832 11.9314 22.6 11.6 22.6C6.29807 22.6 2 18.302 2 13C2 7.69809 6.29807 3.40002 11.6 3.40002C12.3407 3.40002 13.0618 3.48391 13.7543 3.64268C13.6534 4.07884 13.6001 4.53319 13.6001 5C13.6001 8.31371 16.2864 11 19.6001 11ZM11.5999 20.68C13.6754 20.68 15.5585 19.8567 16.9407 18.5189C16.0859 16.4086 14.0167 14.92 11.5998 14.92C9.18298 14.92 7.11378 16.4086 6.25901 18.5189C7.64115 19.8567 9.52436 20.68 11.5999 20.68ZM11.7426 7.41172C10.3168 7.54168 9.2 8.74043 9.2 10.2C9.2 11.7464 10.4536 13 12 13C13.0308 13 13.9315 12.443 14.4176 11.6135C13.0673 10.6058 12.0929 9.12248 11.7426 7.41172Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9C17 13.4183 13.4183 17 9 17C4.58172 17 1 13.4183 1 9C1 4.58172 4.58172 1 9 1C13.4183 1 17 4.58172 17 9ZM5.25 9C5.25 8.58579 5.58579 8.25 6 8.25H8.25V6C8.25 5.58579 8.58579 5.25 9 5.25C9.41421 5.25 9.75 5.58579 9.75 6V8.25H12C12.4142 8.25 12.75 8.58579 12.75 9C12.75 9.41421 12.4142 9.75 12 9.75H9.75V12C9.75 12.4142 9.41421 12.75 9 12.75C8.58579 12.75 8.25 12.4142 8.25 12V9.75H6C5.58579 9.75 5.25 9.41421 5.25 9Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 18.792 11.145 L 2.356 19.359 C 1.249 19.913 0.097 18.725 0.638 17.642 C 0.638 17.642 2.675 13.528 3.235 12.451 C 3.796 11.373 4.437 11.187 10.393 10.417 C 10.614 10.388 10.794 10.222 10.794 10 C 10.794 9.778 10.614 9.612 10.393 9.583 C 4.437 8.813 3.796 8.627 3.235 7.549 C 2.675 6.472 0.638 2.358 0.638 2.358 C 0.097 1.275 1.249 0.087 2.356 0.64 L 18.792 8.855 C 19.736 9.326 19.736 10.674 18.792 11.145 Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 537 B

View file

@ -1,19 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="45px" height="59px" viewBox="-1 -1 45 59" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: bin/sketchtool 1.4 (305) - http://www.bohemiancoding.com/sketch -->
<title>icons_upload_drop</title>
<desc>Created with bin/sketchtool.</desc>
<defs></defs>
<g id="03-Input" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="03_05-File-drop" sketch:type="MSArtboardGroup" transform="translate(-570.000000, -368.000000)">
<g id="icons_upload_drop" sketch:type="MSLayerGroup" transform="translate(570.000000, 368.000000)">
<g id="Rectangle-5-+-Rectangle-6" sketch:type="MSShapeGroup">
<path d="M0,4.00812931 C0,1.79450062 1.78537926,0 4.00241155,0 L24.8253683,0 C24.8253683,0 42.2466793,16.8210687 42.2466793,16.8210687 L42.2466793,53.000599 C42.2466793,55.2094072 40.4583762,57 38.2531894,57 L3.99348992,57 C1.78794634,57 0,55.1999609 0,52.9918707 L0,4.00812931 Z" id="Rectangle-5" stroke="#76CFA6"></path>
<path d="M40.5848017,19.419576 L29.8354335,19.419576 C26.7387692,19.419576 24.2284269,16.9063989 24.2284269,13.8067771 L24.2284269,4.88501382 L40.5848017,19.419576 Z" id="Rectangle-6-Copy" fill="#FFFFFF"></path>
<path d="M42.2466793,18.3870968 L29.539478,18.3870968 C26.4130381,18.3870968 23.8785579,15.8497544 23.8785579,12.7203286 L23.8785579,0" id="Rectangle-6" stroke="#76CFA6"></path>
</g>
<path d="M31.3419737,32.9284726 C31.701384,32.9284726 32.0607942,32.8000473 32.3359677,32.5414375 C32.8825707,32.0259772 32.8825707,31.1920926 32.3359677,30.6766323 L21.622922,20.6119619 C21.076319,20.0965016 20.187153,20.0982608 19.638678,20.6102026 L8.9125289,30.6607991 C8.36405391,31.1762594 8.36218198,32.0119032 8.90878504,32.5273635 C9.4553881,33.0445831 10.344554,33.0445831 10.893029,32.530882 L19.2399573,24.7092556 L19.2437012,46.487014 C19.2437012,47.2153435 19.874541,47.8064516 20.6476474,47.8064516 C21.4244976,47.8064516 22.0515936,47.2153435 22.0515936,46.487014 L22.0478497,24.7426814 L30.3498517,32.5414375 C30.6231533,32.8000473 30.9825635,32.9284726 31.3419737,32.9284726 L31.3419737,32.9284726 Z" id="Fill-75" fill="#76CFA6" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0ZM17.2511 6.97409C16.9775 6.68236 16.5885 6.50012 16.157 6.50012C15.793 6.50012 15.4593 6.62973 15.1996 6.84532C15.1545 6.88267 15.1115 6.92281 15.0707 6.96564L8.79618 13.5539C8.22485 14.1538 8.24801 15.1032 8.8479 15.6746C9.4478 16.2459 10.3973 16.2227 10.9686 15.6228L14.657 11.7501V23.0589C14.657 23.8874 15.3285 24.5589 16.157 24.5589C16.9854 24.5589 17.657 23.8874 17.657 23.0589V11.7502L21.3452 15.6228C21.9165 16.2227 22.866 16.2459 23.4659 15.6746C24.0658 15.1032 24.0889 14.1538 23.5176 13.5539L17.2511 6.97409Z" fill="#0DBD8B"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 802 B

Before After
Before After

View file

@ -123,6 +123,7 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b;

View file

@ -120,6 +120,7 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b;

View file

@ -187,6 +187,7 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b;

View file

@ -67,9 +67,6 @@ $groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77);
// used by RoomDirectory permissions
$plinth-bg-color: $secondary-accent-color;
// used by RoomDropTarget
$droptarget-bg-color: rgba(255,255,255,0.5);
// used by AddressSelector
$selected-color: $secondary-accent-color;
@ -181,6 +178,7 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b;

View file

@ -16,6 +16,10 @@
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
}
.mx_SpacePanel {
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
}
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
backdrop-filter: blur($roomlist-background-blur-amount);
}

View file

@ -38,6 +38,7 @@ import UserActivity from "../UserActivity";
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
import {SpaceStoreClass} from "../stores/SpaceStore";
declare global {
interface Window {
@ -68,6 +69,7 @@ declare global {
mxUserActivity: UserActivity;
mxModalWidgetStore: ModalWidgetStore;
mxVoipUserMapper: VoipUserMapper;
mxSpaceStore: SpaceStoreClass;
}
interface Document {

View file

@ -165,6 +165,9 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
return explicitRoomAvatar;
}
// space rooms cannot be DMs so skip the rest
if (room.isSpaceRoom()) return null;
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {

View file

@ -131,6 +131,14 @@ export default abstract class BasePlatform {
hideUpdateToast();
}
/**
* Return true if platform supports multi-language
* spell-checking, otherwise false.
*/
supportsMultiLanguageSpellCheck(): boolean {
return false;
}
/**
* Returns true if the platform supports displaying
* notifications, otherwise false.
@ -240,6 +248,16 @@ export default abstract class BasePlatform {
setLanguage(preferredLangs: string[]) {}
setSpellCheckLanguages(preferredLangs: string[]) {}
getSpellCheckLanguages(): Promise<string[]> | null {
return null;
}
getAvailableSpellCheckLanguages(): Promise<string[]> | null {
return null;
}
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
const url = new URL(window.location.href);
url.hash = fragmentAfterLogin || "";

View file

@ -629,6 +629,8 @@ export default class CallHandler {
const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds");
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
this.calls.set(roomId, call);
@ -704,6 +706,14 @@ export default class CallHandler {
return;
}
if (this.getCallForRoom(room.roomId)) {
Modal.createTrackedDialog('Call Handler', 'Existing Call with user', ErrorDialog, {
title: _t('Already in call'),
description: _t("You're already in a call with this person."),
});
return;
}
const members = room.getJoinedMembers();
if (members.length <= 1) {
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {

View file

@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
import Modal from './Modal';
import * as sdk from './';
import { _t } from './languageHandler';
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
@ -50,10 +50,13 @@ export function showStartChatInviteDialog(initialText) {
}
export function showRoomInviteDialog(roomId) {
const isSpace = MatrixClientPeg.get()?.getRoom(roomId)?.isSpaceRoom();
// This dialog handles the room creation internally - we don't need to worry about it.
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
Modal.createTrackedDialog(
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
"Invite Users", isSpace ? "Space" : "Room", InviteDialog, {
kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE,
roomId,
},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
);
}

View file

@ -98,11 +98,27 @@ async function getSecretStorageKey(
{ keys: keyInfos }: { keys: Record<string, ISecretStorageKeyInfo> },
ssssItemName,
): Promise<[string, Uint8Array]> {
const cli = MatrixClientPeg.get();
let keyId = await cli.getDefaultSecretStorageKeyId();
let keyInfo;
if (keyId) {
// use the default SSSS key if set
keyInfo = keyInfos[keyId];
if (!keyInfo) {
// if the default key is not available, pretend the default key
// isn't set
keyId = undefined;
}
}
if (!keyId) {
// if no default SSSS key is set, fall back to a heuristic of using the
// only available key, if only one key is set
const keyInfoEntries = Object.entries(keyInfos);
if (keyInfoEntries.length > 1) {
throw new Error("Multiple storage key requests not implemented");
}
const [keyId, keyInfo] = keyInfoEntries[0];
[keyId, keyInfo] = keyInfoEntries[0];
}
// Check the in-memory cache
if (isCachingAllowed() && secretStorageKeys[keyId]) {

View file

@ -118,25 +118,10 @@ export default class Velociraptor extends React.Component {
domNode.style.visibility = restingStyle.visibility;
});
/*
console.log("enter:",
JSON.stringify(transitionOpts[i-1]),
"->",
JSON.stringify(restingStyle));
*/
} else if (node === null) {
// Velocity stores data on elements using the jQuery .data()
// method, and assumes you'll be using jQuery's .remove() to
// remove the element, but we don't use jQuery, so we need to
// blow away the element's data explicitly otherwise it will leak.
// This uses Velocity's internal jQuery compatible wrapper.
// See the bug at
// https://github.com/julianshapiro/velocity/issues/300
// and the FAQ entry, "Preventing memory leaks when
// creating/destroying large numbers of elements"
// (https://github.com/julianshapiro/velocity/issues/47)
const domNode = ReactDom.findDOMNode(this.nodes[k]);
if (domNode) Velocity.Utilities.removeData(domNode);
// console.log("enter:",
// JSON.stringify(transitionOpts[i-1]),
// "->",
// JSON.stringify(restingStyle));
}
this.nodes[k] = node;
}

View file

@ -19,14 +19,23 @@ limitations under the License.
import React from "react";
import AccessibleButton from "../../components/views/elements/AccessibleButton";
import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
label?: string;
tooltip?: string;
}
// Semantic component for representing a role=menuitem
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
export const MenuItem: React.FC<IProps> = ({children, label, tooltip, ...props}) => {
const ariaLabel = props["aria-label"] || label;
if (tooltip) {
return <AccessibleTooltipButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel} title={tooltip}>
{ children }
</AccessibleTooltipButton>;
}
return (
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
{ children }

View file

@ -155,6 +155,7 @@ export default class UserProvider extends AutocompleteProvider {
const currentUserId = MatrixClientPeg.get().credentials.userId;
this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
this.users = this.users.concat(this.room.getMembersWithMembership("invite"));
this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);

View file

@ -76,6 +76,7 @@ export interface IProps extends IPosition {
hasBackground?: boolean;
// whether this context menu should be focus managed. If false it must handle itself
managed?: boolean;
wrapperClassName?: string;
// Function to be called on menu close
onFinished();
@ -365,7 +366,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
return (
<div
className="mx_ContextualMenu_wrapper"
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
style={{...position, ...wrapperStyle}}
onKeyDown={this.onKeyDown}
onContextMenu={this.onContextMenuPreventBubbling}

View file

@ -39,6 +39,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import SpacePanel from "../views/spaces/SpacePanel";
interface IProps {
isMinimized: boolean;
@ -388,12 +389,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
public render(): React.ReactNode {
const groupFilterPanel = !this.state.showGroupFilterPanel ? null : (
let leftLeftPanel;
// Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now
// ignore it and force the rendering of SpacePanel if that Labs flag is enabled.
if (SettingsStore.getValue("feature_spaces")) {
leftLeftPanel = <SpacePanel />;
} else if (this.state.showGroupFilterPanel) {
leftLeftPanel = (
<div className="mx_LeftPanel_GroupFilterPanelContainer">
<GroupFilterPanel />
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
</div>
);
}
const roomList = <RoomList
onKeyDown={this.onKeyDown}
@ -406,7 +414,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const containerClasses = classNames({
"mx_LeftPanel": true,
"mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel,
"mx_LeftPanel_minimized": this.props.isMinimized,
});
@ -417,7 +424,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return (
<div className={containerClasses}>
{groupFilterPanel}
{leftLeftPanel}
<aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}

View file

@ -55,6 +55,7 @@ import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { IOpts } from "../../createRoom";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -91,11 +92,14 @@ interface IProps {
currentGroupId?: string;
currentGroupIsNew?: boolean;
justRegistered?: boolean;
roomJustCreatedOpts?: IOpts;
}
interface IUsageLimit {
// "hs_disabled" is NOT a specced string, but is used in Synapse
// This is tracked over at https://github.com/matrix-org/synapse/issues/9237
// eslint-disable-next-line camelcase
limit_type: "monthly_active_user" | string;
limit_type: "monthly_active_user" | "hs_disabled" | string;
// eslint-disable-next-line camelcase
admin_contact?: string;
}
@ -103,11 +107,15 @@ interface IUsageLimit {
interface IState {
syncErrorData?: {
error: {
// This is not specced, but used in Synapse. See
// https://github.com/matrix-org/synapse/issues/9237#issuecomment-768238922
data: IUsageLimit;
errcode: string;
};
};
usageLimitDismissed: boolean;
usageLimitEventContent?: IUsageLimit;
usageLimitEventTs?: number;
useCompactLayout: boolean;
}
@ -151,6 +159,7 @@ class LoggedInView extends React.Component<IProps, IState> {
syncErrorData: undefined,
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
usageLimitDismissed: false,
};
// stash the MatrixClient in case we log out before we are unmounted
@ -218,7 +227,14 @@ class LoggedInView extends React.Component<IProps, IState> {
let size;
let collapsed;
const collapseConfig: ICollapseConfig = {
toggleSize: 260 - 50,
// TODO: the space panel currently does not have a fixed width,
// just the headers at each level have a max-width of 150px
// Taking 222px for the space panel for now,
// so this will look slightly off for now,
// depending on the depth of your space tree.
// To fix this, we'll need to turn toggleSize
// into a callback so it can be measured when starting the resize operation
toggleSize: 222 + 68,
onCollapsed: (_collapsed) => {
collapsed = _collapsed;
if (_collapsed) {
@ -239,6 +255,9 @@ class LoggedInView extends React.Component<IProps, IState> {
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
this.props.resizeNotifier.stopResizing();
},
isItemCollapsed: domNode => {
return domNode.classList.contains("mx_LeftPanel_minimized");
},
};
const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig);
resizer.setClassNames({
@ -302,14 +321,27 @@ class LoggedInView extends React.Component<IProps, IState> {
}
};
private onUsageLimitDismissed = () => {
this.setState({
usageLimitDismissed: true,
});
}
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncError.error.data;
}
if (usageLimitEventContent) {
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
// usageLimitDismissed is true when the user has explicitly hidden the toast
// and it will be reset to false if a *new* usage alert comes in.
if (usageLimitEventContent && this.state.usageLimitDismissed) {
showServerLimitToast(
usageLimitEventContent.limit_type,
this.onUsageLimitDismissed,
usageLimitEventContent.admin_contact,
error,
);
} else {
hideServerLimitToast();
}
@ -320,10 +352,12 @@ class LoggedInView extends React.Component<IProps, IState> {
if (!serverNoticeList) return [];
const events = [];
let pinnedEventTs = 0;
for (const room of serverNoticeList) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
pinnedEventTs = pinStateEvent.getTs();
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
@ -333,6 +367,11 @@ class LoggedInView extends React.Component<IProps, IState> {
}
}
if (pinnedEventTs && this.state.usageLimitEventTs > pinnedEventTs) {
// We've processed a newer event than this one, so ignore it.
return;
}
const usageLimitEvent = events.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
@ -341,7 +380,12 @@ class LoggedInView extends React.Component<IProps, IState> {
});
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
this.setState({ usageLimitEventContent });
this.setState({
usageLimitEventContent,
usageLimitEventTs: pinnedEventTs,
// This is a fresh toast, we can show toasts again
usageLimitDismissed: false,
});
};
_onPaste = (ev) => {
@ -591,6 +635,7 @@ class LoggedInView extends React.Component<IProps, IState> {
viaServers={this.props.viaServers}
key={this.props.currentRoomId || 'roomview'}
resizeNotifier={this.props.resizeNotifier}
justCreatedOpts={this.props.roomJustCreatedOpts}
/>;
break;

View file

@ -48,7 +48,7 @@ import * as Lifecycle from '../../Lifecycle';
import '../../stores/LifecycleStore';
import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom";
import createRoom, {IOpts} from "../../createRoom";
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
import SettingsStore from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
@ -82,6 +82,8 @@ import {UIFeature} from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory";
/** constants for MatrixChat.state.view */
export enum Views {
@ -144,6 +146,8 @@ interface IRoomInfo {
oob_data?: object;
via_servers?: string[];
threepid_invite?: IThreepidInvite;
justCreatedOpts?: IOpts;
}
/* eslint-enable camelcase */
@ -201,6 +205,7 @@ interface IState {
viaServers?: string[];
pendingInitialSync?: boolean;
justRegistered?: boolean;
roomJustCreatedOpts?: IOpts;
}
export default class MatrixChat extends React.PureComponent<IProps, IState> {
@ -688,10 +693,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break;
}
case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace) {
Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, {
space: SpaceStore.instance.activeSpace,
initialText: payload.initialText,
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
} else {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText,
}, 'mx_RoomDirectory_dialogWrapper', false, true);
}
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
@ -922,6 +934,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
ready: true,
roomJustCreatedOpts: roomInfo.justCreatedOpts,
}, () => {
this.notifyNewScreen('room/' + presentedId, replaceLast);
});
@ -1068,6 +1081,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private leaveRoomWarnings(roomId: string) {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const isSpace = roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications.
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
const warnings = [];
@ -1077,7 +1091,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
warnings.push((
<span className="warning" key="non_public_warning">
{' '/* Whitespace, otherwise the sentences get smashed together */ }
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
{ isSpace
? _t("This space is not public. You will not be able to rejoin without an invite.")
: _t("This room is not public. You will not be able to rejoin without an invite.") }
</span>
));
}
@ -1090,11 +1106,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId);
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
title: _t("Leave room"),
const isSpace = roomToLeave?.isSpaceRoom();
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
title: isSpace ? _t("Leave space") : _t("Leave room"),
description: (
<span>
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
{ isSpace
? _t("Are you sure you want to leave the space '%(spaceName)s'?", {spaceName: roomToLeave.name})
: _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
{ warnings }
</span>
),
@ -1108,6 +1127,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.finally(() => modal.close());
dis.dispatch({
action: "after_leave_room",
room_id: roomId,
});
}
},
});

View file

@ -24,7 +24,11 @@ import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import {
RightPanelPhases,
RIGHT_PANEL_PHASES_NO_ARGS,
RIGHT_PANEL_SPACE_PHASES,
} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
@ -79,6 +83,8 @@ export default class RightPanel extends React.Component {
return RightPanelPhases.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
return RightPanelPhases.SpaceMemberList;
} else if (userForPanel) {
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
// from its props and some from a store, except if the contents of the store changes
@ -99,9 +105,8 @@ export default class RightPanel extends React.Component {
return rps.roomPanelPhase;
}
return RightPanelPhases.RoomMemberInfo;
} else {
return rps.roomPanelPhase;
}
return rps.roomPanelPhase;
}
componentDidMount() {
@ -181,6 +186,7 @@ export default class RightPanel extends React.Component {
verificationRequest: payload.verificationRequest,
verificationRequestPromise: payload.verificationRequestPromise,
widgetId: payload.widgetId,
space: payload.space,
});
}
}
@ -232,6 +238,13 @@ export default class RightPanel extends React.Component {
panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
}
break;
case RightPanelPhases.SpaceMemberList:
panel = <MemberList
roomId={this.state.space ? this.state.space.roomId : roomId}
key={this.state.space ? this.state.space.roomId : roomId}
onClose={this.onClose}
/>;
break;
case RightPanelPhases.GroupMemberList:
if (this.props.groupId) {
@ -244,10 +257,11 @@ export default class RightPanel extends React.Component {
break;
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.SpaceMemberInfo:
case RightPanelPhases.EncryptionPanel:
panel = <UserInfo
user={this.state.member}
room={this.props.room}
room={this.state.phase === RightPanelPhases.SpaceMemberInfo ? this.state.space : this.props.room}
key={roomId || this.state.member.userId}
onClose={this.onClose}
phase={this.state.phase}
@ -257,6 +271,7 @@ export default class RightPanel extends React.Component {
break;
case RightPanelPhases.Room3pidMemberInfo:
case RightPanelPhases.Space3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={roomId} />;
break;

View file

@ -195,6 +195,10 @@ export default class RoomStatusBar extends React.Component {
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'hs_disabled': _td(
"Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'': _td(
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",

View file

@ -34,7 +34,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import * as sdk from '../../index';
import CallHandler from '../../CallHandler';
import CallHandler, { PlaceCallType } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher';
import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc';
@ -80,6 +80,8 @@ import { showToast as showNotificationsToast } from "../../toasts/DesktopNotific
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -114,6 +116,7 @@ interface IProps {
autoJoin?: boolean;
resizeNotifier: ResizeNotifier;
justCreatedOpts?: IOpts;
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
onRegistered?(credentials: IMatrixClientCreds): void;
@ -189,6 +192,7 @@ export interface IState {
rejecting?: boolean;
rejectError?: Error;
hasPinnedWidgets?: boolean;
dragCounter: number;
}
export default class RoomView extends React.Component<IProps, IState> {
@ -239,6 +243,7 @@ export default class RoomView extends React.Component<IProps, IState> {
canReply: false,
layout: SettingsStore.getValue("layout"),
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0,
};
this.dispatcherRef = dis.register(this.onAction);
@ -532,8 +537,8 @@ export default class RoomView extends React.Component<IProps, IState> {
if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver);
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
roomView.addEventListener('dragenter', this.onDragEnter);
roomView.addEventListener('dragleave', this.onDragLeave);
}
}
@ -577,8 +582,8 @@ export default class RoomView extends React.Component<IProps, IState> {
const roomView = this.roomView.current;
roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
roomView.removeEventListener('dragenter', this.onDragEnter);
roomView.removeEventListener('dragleave', this.onDragLeave);
}
dis.unregister(this.dispatcherRef);
if (this.context) {
@ -1138,6 +1143,31 @@ export default class RoomView extends React.Component<IProps, IState> {
this.updateTopUnreadMessagesBar();
};
private onDragEnter = ev => {
ev.stopPropagation();
ev.preventDefault();
this.setState({
dragCounter: this.state.dragCounter + 1,
draggingFile: true,
});
};
private onDragLeave = ev => {
ev.stopPropagation();
ev.preventDefault();
this.setState({
dragCounter: this.state.dragCounter - 1,
});
if (this.state.dragCounter === 0) {
this.setState({
draggingFile: false,
});
}
};
private onDragOver = ev => {
ev.stopPropagation();
ev.preventDefault();
@ -1145,7 +1175,6 @@ export default class RoomView extends React.Component<IProps, IState> {
ev.dataTransfer.dropEffect = 'none';
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy';
}
};
@ -1156,14 +1185,12 @@ export default class RoomView extends React.Component<IProps, IState> {
ContentMessages.sharedInstance().sendContentListToRoom(
ev.dataTransfer.files, this.state.room.roomId, this.context,
);
this.setState({ draggingFile: false });
dis.fire(Action.FocusComposer);
};
private onDragLeaveOrEnd = ev => {
ev.stopPropagation();
ev.preventDefault();
this.setState({ draggingFile: false });
this.setState({
draggingFile: false,
dragCounter: this.state.dragCounter - 1,
});
};
private injectSticker(url, info, text) {
@ -1352,6 +1379,14 @@ export default class RoomView extends React.Component<IProps, IState> {
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
};
private onCallPlaced = (type: PlaceCallType) => {
dis.dispatch({
action: 'place_call',
type: type,
room_id: this.state.room.roomId,
});
};
private onSettingsClick = () => {
dis.dispatch({ action: "open_room_settings" });
};
@ -1389,7 +1424,7 @@ export default class RoomView extends React.Component<IProps, IState> {
});
};
private onRejectButtonClicked = ev => {
private onRejectButtonClicked = () => {
this.setState({
rejecting: true,
});
@ -1449,7 +1484,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}
};
private onRejectThreepidInviteButtonClicked = ev => {
private onRejectThreepidInviteButtonClicked = () => {
// We can reject 3pid invites in the same way that we accept them,
// using /leave rather than /join. In the short term though, we
// just ignore them.
@ -1712,7 +1747,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}
const myMembership = this.state.room.getMyMembership();
if (myMembership == 'invite') {
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
if (this.state.joining || this.state.rejecting) {
return (
<ErrorBoundary>
@ -1757,6 +1792,19 @@ export default class RoomView extends React.Component<IProps, IState> {
}
}
let fileDropTarget = null;
if (this.state.draggingFile) {
fileDropTarget = (
<div className="mx_RoomView_fileDropTarget">
<img
src={require("../../../res/img/upload-big.svg")}
className="mx_RoomView_fileDropTarget_image"
/>
{ _t("Drop file here to upload") }
</div>
);
}
// We have successfully loaded this room, and are not previewing.
// Display the "normal" room view.
@ -1841,7 +1889,7 @@ export default class RoomView extends React.Component<IProps, IState> {
room={this.state.room}
/>
);
if (!this.state.canPeek) {
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
return (
<div className="mx_RoomView">
{ previewBar }
@ -1863,12 +1911,23 @@ export default class RoomView extends React.Component<IProps, IState> {
);
}
if (this.state.room?.isSpaceRoom()) {
return <SpaceRoomView
space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts}
resizeNotifier={this.props.resizeNotifier}
onJoinButtonClicked={this.onJoinButtonClicked}
onRejectButtonClicked={this.props.threepidInvite
? this.onRejectThreepidInviteButtonClicked
: this.onRejectButtonClicked}
/>;
}
const auxPanel = (
<AuxPanel
room={this.state.room}
fullHeight={false}
userId={this.context.credentials.userId}
draggingFile={this.state.draggingFile}
maxHeight={this.state.auxPanelMaxHeight}
showApps={this.state.showApps}
onResize={this.onResize}
@ -2031,11 +2090,13 @@ export default class RoomView extends React.Component<IProps, IState> {
e2eStatus={this.state.e2eStatus}
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
appsShown={this.state.showApps}
onCallPlaced={this.onCallPlaced}
/>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className="mx_RoomView_body">
{auxPanel}
<div className={timelineClasses}>
{fileDropTarget}
{topUnreadMessagesBar}
{jumpToBottom}
{messagePanel}

View file

@ -0,0 +1,576 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useMemo, useRef, useState} from "react";
import Room from "matrix-js-sdk/src/models/room";
import MatrixEvent from "matrix-js-sdk/src/models/event";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import {_t} from "../../languageHandler";
import AccessibleButton from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog";
import FormButton from "../views/elements/FormButton";
import SearchBox from "./SearchBox";
import RoomAvatar from "../views/avatars/RoomAvatar";
import RoomName from "../views/elements/RoomName";
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
import {shouldShowSpaceSettings} from "../../utils/space";
import {EnhancedMap} from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar";
interface IProps {
space: Room;
initialText?: string;
onFinished(): void;
}
/* eslint-disable camelcase */
export interface ISpaceSummaryRoom {
canonical_alias?: string;
aliases: string[];
avatar_url?: string;
guest_can_join: boolean;
name?: string;
num_joined_members: number
room_id: string;
topic?: string;
world_readable: boolean;
num_refs: number;
room_type: string;
}
export interface ISpaceSummaryEvent {
room_id: string;
event_id: string;
origin_server_ts: number;
type: string;
state_key: string;
content: {
order?: string;
auto_join?: boolean;
via?: string;
};
}
/* eslint-enable camelcase */
interface ISubspaceProps {
space: ISpaceSummaryRoom;
event?: MatrixEvent;
editing?: boolean;
onPreviewClick?(): void;
queueAction?(action: IAction): void;
onJoinClick?(): void;
}
const SubSpace: React.FC<ISubspaceProps> = ({
space,
editing,
event,
queueAction,
onJoinClick,
onPreviewClick,
children,
}) => {
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get();
const cliRoom = cli.getRoom(space.room_id);
const myMembership = cliRoom?.getMyMembership();
// TODO DRY code
let actions;
if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => {
_setAutoJoin(v => {
queueAction({
event,
removed,
autoJoin: !v,
});
return !v;
});
};
const setRemoved = () => {
_setRemoved(v => {
queueAction({
event,
removed: !v,
autoJoin,
});
return !v;
});
};
if (removed) {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
</React.Fragment>;
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
</React.Fragment>;
}
} else {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("No permissions")}
</span>;
}
// TODO confirm remove from space click behaviour here
} else {
if (myMembership === "join") {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("You're in this space")}
</span>;
} else if (onJoinClick) {
actions = <React.Fragment>
<AccessibleButton onClick={onPreviewClick} kind="link">
{ _t("Preview") }
</AccessibleButton>
<FormButton onClick={onJoinClick} label={_t("Join")} />
</React.Fragment>
}
}
let url: string;
if (space.avatar_url) {
url = MatrixClientPeg.get().mxcUrlToHttp(
space.avatar_url,
Math.floor(24 * window.devicePixelRatio),
Math.floor(24 * window.devicePixelRatio),
"crop",
);
}
return <div className="mx_SpaceRoomDirectory_subspace">
<div className="mx_SpaceRoomDirectory_subspace_info">
<BaseAvatar name={name} idName={space.room_id} url={url} width={24} height={24} />
{ name }
<div className="mx_SpaceRoomDirectory_actions">
{ actions }
</div>
</div>
<div className="mx_SpaceRoomDirectory_subspace_children">
{ children }
</div>
</div>
};
interface IAction {
event: MatrixEvent;
removed: boolean;
autoJoin: boolean;
}
interface IRoomTileProps {
room: ISpaceSummaryRoom;
event?: MatrixEvent;
editing?: boolean;
onPreviewClick(): void;
queueAction?(action: IAction): void;
onJoinClick?(): void;
}
const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => {
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get();
const cliRoom = cli.getRoom(room.room_id);
const myMembership = cliRoom?.getMyMembership();
let actions;
if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => {
_setAutoJoin(v => {
queueAction({
event,
removed,
autoJoin: !v,
});
return !v;
});
};
const setRemoved = () => {
_setRemoved(v => {
queueAction({
event,
removed: !v,
autoJoin,
});
return !v;
});
};
if (removed) {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
</React.Fragment>;
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
</React.Fragment>;
}
} else {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("No permissions")}
</span>;
}
// TODO confirm remove from space click behaviour here
} else {
if (myMembership === "join") {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("You're in this room")}
</span>;
} else if (onJoinClick) {
actions = <React.Fragment>
<AccessibleButton onClick={onPreviewClick} kind="link">
{ _t("Preview") }
</AccessibleButton>
<FormButton onClick={onJoinClick} label={_t("Join")} />
</React.Fragment>
}
}
let url: string;
if (room.avatar_url) {
url = cli.mxcUrlToHttp(
room.avatar_url,
Math.floor(32 * window.devicePixelRatio),
Math.floor(32 * window.devicePixelRatio),
"crop",
);
}
const content = <React.Fragment>
<BaseAvatar name={name} idName={room.room_id} url={url} width={32} height={32} />
<div className="mx_SpaceRoomDirectory_roomTile_info">
<div className="mx_SpaceRoomDirectory_roomTile_name">
{ name }
</div>
<div className="mx_SpaceRoomDirectory_roomTile_topic">
{ room.topic }
</div>
</div>
<div className="mx_SpaceRoomDirectory_roomTile_memberCount">
{ room.num_joined_members }
</div>
<div className="mx_SpaceRoomDirectory_actions">
{ actions }
</div>
</React.Fragment>;
if (editing) {
return <div className="mx_SpaceRoomDirectory_roomTile">
{ content }
</div>
}
return <AccessibleButton className="mx_SpaceRoomDirectory_roomTile" onClick={onPreviewClick}>
{ content }
</AccessibleButton>;
};
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
// Don't let the user view a room they won't be able to either peek or join:
// fail earlier so they don't have to click back to the directory.
if (MatrixClientPeg.get().isGuest()) {
if (!room.world_readable && !room.guest_can_join) {
dis.dispatch({ action: "require_registration" });
return;
}
}
const roomAlias = getDisplayAliasForRoom(room) || undefined;
dis.dispatch({
action: "view_room",
auto_join: autoJoin,
should_peek: true,
_type: "room_directory", // instrumentation
room_alias: roomAlias,
room_id: room.room_id,
via_servers: viaServers,
oob_data: {
avatarUrl: room.avatar_url,
// XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
name: room.name || roomAlias || _t("Unnamed room"),
},
});
};
interface IHierarchyLevelProps {
spaceId: string;
rooms: Map<string, ISpaceSummaryRoom>;
editing?: boolean;
relations: EnhancedMap<string, string[]>;
parents: Set<string>;
queueAction?(action: IAction): void;
onPreviewClick(roomId: string): void;
onRemoveFromSpaceClick?(roomId: string): void;
onJoinClick?(roomId: string): void;
}
export const HierarchyLevel = ({
spaceId,
rooms,
editing,
relations,
parents,
onPreviewClick,
onJoinClick,
queueAction,
}: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get();
const space = cli.getRoom(spaceId);
// TODO respect order
const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
if (!rooms.has(roomId)) return result; // TODO wat
result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
return result;
}, [[], []]) || [[], []];
// Don't render this subspace if it has no rooms we can show
// TODO this is broken - as a space may have subspaces we still need to show
// if (!childRooms.length) return null;
const userId = cli.getUserId();
const newParents = new Set(parents).add(spaceId);
return <React.Fragment>
{
childRooms.map(roomId => (
<RoomTile
key={roomId}
room={rooms.get(roomId)}
event={space?.currentState.maySendStateEvent(EventType.SpaceChild, userId)
? space?.currentState.getStateEvents(EventType.SpaceChild, roomId)
: undefined}
editing={editing}
queueAction={queueAction}
onPreviewClick={() => {
onPreviewClick(roomId);
}}
onJoinClick={onJoinClick ? () => {
onJoinClick(roomId);
} : undefined}
/>
))
}
{
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
<SubSpace
key={roomId}
space={rooms.get(roomId)}
event={space?.currentState.getStateEvents(EventType.SpaceChild, roomId)}
editing={editing}
queueAction={queueAction}
onPreviewClick={() => {
onPreviewClick(roomId);
}}
onJoinClick={() => {
onJoinClick(roomId);
}}
>
<HierarchyLevel
spaceId={roomId}
rooms={rooms}
editing={editing}
relations={relations}
parents={newParents}
onPreviewClick={onPreviewClick}
onJoinClick={onJoinClick}
queueAction={queueAction}
/>
</SubSpace>
))
}
</React.Fragment>
};
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
// TODO pagination
const cli = MatrixClientPeg.get();
const [query, setQuery] = useState(initialText);
const [isEditing, setIsEditing] = useState(false);
const onCreateRoomClick = () => {
dis.dispatch({
action: 'view_create_room',
public: true,
});
onFinished();
};
// stored within a ref as we don't need to re-render when it changes
const pendingActions = useRef(new Map<string, IAction>());
let adminButton;
if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test
const onManageButtonClicked = () => {
setIsEditing(true);
};
const onSaveButtonClicked = () => {
// TODO setBusy
pendingActions.current.forEach(({event, autoJoin, removed}) => {
const content = {
...event.getContent(),
auto_join: autoJoin,
};
if (removed) {
delete content["via"];
}
cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey());
});
setIsEditing(false);
};
if (isEditing) {
adminButton = <React.Fragment>
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
<span>{ _t("All users join by default") }</span>
</React.Fragment>;
} else {
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
}
}
const [rooms, relations, viaMap] = useAsyncMemo(async () => {
try {
const data = await cli.getSpaceSummary(space.roomId);
const parentChildRelations = new EnhancedMap<string, string[]>();
const viaMap = new EnhancedMap<string, Set<string>>();
data.events.map((ev: ISpaceSummaryEvent) => {
if (ev.type === EventType.SpaceChild) {
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
}
if (Array.isArray(ev.content["via"])) {
const set = viaMap.getOrCreate(ev.state_key, new Set());
ev.content["via"].forEach(via => set.add(via));
}
});
return [data.rooms, parentChildRelations, viaMap];
} catch (e) {
console.error(e); // TODO
}
return [];
}, [space], []);
const roomsMap = useMemo(() => {
if (!rooms) return null;
const lcQuery = query.toLowerCase();
const filteredRooms = rooms.filter(r => {
return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms
|| r.name?.toLowerCase().includes(lcQuery)
|| r.topic?.toLowerCase().includes(lcQuery);
});
return new Map<string, ISpaceSummaryRoom>(filteredRooms.map(r => [r.room_id, r]));
// const root = rooms.get(space.roomId);
}, [rooms, query]);
const title = <React.Fragment>
<RoomAvatar room={space} height={40} width={40} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
const explanation =
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
{a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
);
let content;
if (roomsMap) {
content = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
editing={isEditing}
relations={relations}
parents={new Set()}
queueAction={action => {
pendingActions.current.set(action.event.room_id, action);
}}
onPreviewClick={roomId => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false);
onFinished();
}}
onJoinClick={(roomId) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
onFinished();
}}
/>
</AutoHideScrollbar>;
}
// TODO loading state/error state
return (
<BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}>
<div className="mx_Dialog_content">
{ explanation }
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Find a room...") }
onSearch={setQuery}
/>
<div className="mx_SpaceRoomDirectory_listHeader">
{ adminButton }
</div>
{ content }
</div>
</BaseDialog>
);
};
export default SpaceRoomDirectory;
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list
function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
}

View file

@ -0,0 +1,604 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {RefObject, useContext, useRef, useState} from "react";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import {Room} from "matrix-js-sdk/src/models/room";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomAvatar from "../views/avatars/RoomAvatar";
import {_t} from "../../languageHandler";
import AccessibleButton from "../views/elements/AccessibleButton";
import RoomName from "../views/elements/RoomName";
import RoomTopic from "../views/elements/RoomTopic";
import InlineSpinner from "../views/elements/InlineSpinner";
import FormButton from "../views/elements/FormButton";
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
import {useRoomMembers} from "../../hooks/useRoomMembers";
import createRoom, {IOpts, Preset} from "../../createRoom";
import Field from "../views/elements/Field";
import {useEventEmitter} from "../../hooks/useEventEmitter";
import StyledRadioGroup from "../views/elements/StyledRadioGroup";
import withValidation from "../views/elements/Validation";
import * as Email from "../../email";
import defaultDispatcher from "../../dispatcher/dispatcher";
import {Action} from "../../dispatcher/actions";
import ResizeNotifier from "../../utils/ResizeNotifier"
import MainSplit from './MainSplit';
import ErrorBoundary from "../views/elements/ErrorBoundary";
import {ActionPayload} from "../../dispatcher/payloads";
import RightPanel from "./RightPanel";
import RightPanelStore from "../../stores/RightPanelStore";
import {EventSubscription} from "fbemitter";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
import {useStateArray} from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare";
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory";
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
import {EnhancedMap} from "../../utils/maps";
import AutoHideScrollbar from "./AutoHideScrollbar";
import MemberAvatar from "../views/avatars/MemberAvatar";
import {useStateToggle} from "../../hooks/useStateToggle";
interface IProps {
space: Room;
justCreatedOpts?: IOpts;
resizeNotifier: ResizeNotifier;
onJoinButtonClicked(): void;
onRejectButtonClicked(): void;
}
interface IState {
phase: Phase;
showRightPanel: boolean;
}
enum Phase {
Landing,
PublicCreateRooms,
PublicShare,
PrivateScope,
PrivateInvite,
PrivateCreateRooms,
PrivateExistingRooms,
}
const RoomMemberCount = ({ room, children }) => {
const members = useRoomMembers(room);
const count = members.length;
if (children) return children(count);
return count;
};
const useMyRoomMembership = (room: Room) => {
const [membership, setMembership] = useState(room.getMyMembership());
useEventEmitter(room, "Room.myMembership", () => {
setMembership(room.getMyMembership());
});
return membership;
};
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
const joinRule = space.getJoinRule();
const userId = cli.getUserId();
let joinButtons;
if (myMembership === "invite") {
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
{_t("Decline")}
</AccessibleButton>
</div>;
} else if (myMembership !== "join" && joinRule === "public") {
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
</div>;
}
let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = (
<AccessibleButton className="mx_SpaceRoomView_landing_inviteButton" onClick={() => {
showRoomInviteDialog(space.roomId);
}}>
{ _t("Invite people") }
</AccessibleButton>
);
}
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const [_, forceUpdate] = useStateToggle(false); // TODO
let addRoomButtons;
if (canAddRooms) {
addRoomButtons = <React.Fragment>
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
const [added] = await showAddExistingRooms(cli, space);
if (added) {
forceUpdate();
}
}}>
{ _t("Add existing rooms & spaces") }
</AccessibleButton>
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
showCreateNewRoom(cli, space);
}}>
{ _t("Create a new room") }
</AccessibleButton>
</React.Fragment>;
}
let settingsButton;
if (shouldShowSpaceSettings(cli, space)) {
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
showSpaceSettings(cli, space);
}}>
{ _t("Settings") }
</AccessibleButton>;
}
const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => {
try {
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
const parentChildRelations = new EnhancedMap<string, string[]>();
data.events.map((ev: ISpaceSummaryEvent) => {
if (ev.type === EventType.SpaceChild) {
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
}
});
const roomsMap = new Map<string, ISpaceSummaryRoom>(data.rooms.map(r => [r.room_id, r]));
const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length;
return [false, roomsMap, parentChildRelations, numRooms];
} catch (e) {
console.error(e); // TODO
}
return [false];
}, [space, _], [true]);
let previewRooms;
if (roomsMap) {
previewRooms = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
<div className="mx_SpaceRoomDirectory_roomCount">
<h3>{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}</h3>
<span>{ numRooms }</span>
</div>
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
editing={false}
relations={relations}
parents={new Set()}
onPreviewClick={roomId => {
showRoom(roomsMap.get(roomId), [], false); // TODO
}}
/>
</AutoHideScrollbar>;
} else if (loading) {
previewRooms = <InlineSpinner />;
} else {
previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
}
return <div className="mx_SpaceRoomView_landing">
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<div className="mx_SpaceRoomView_landing_name">
<RoomName room={space}>
{(name) => {
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
<h1>{ name }</h1>
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_landing_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div> };
if (myMembership === "invite") {
const inviteSender = space.getMember(userId)?.events.member?.getSender();
const inviter = inviteSender && space.getMember(inviteSender);
if (inviteSender) {
return _t("<inviter/> invited you to <name/>", {}, {
name: tags.name,
inviter: () => inviter
? <span className="mx_SpaceRoomView_landing_inviter">
<MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
{ inviter.name }
</span>
: <span className="mx_SpaceRoomView_landing_inviter">
{ inviteSender }
</span>,
}) as JSX.Element;
} else {
return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
}
} else if (shouldShowSpaceSettings(cli, space)) {
if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else {
return _t("Your private space <name/>", {}, tags) as JSX.Element;
}
}
return _t("Welcome to <name/>", {}, tags) as JSX.Element;
}}
</RoomName>
</div>
<div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} />
</div>
{ joinButtons }
<div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton }
{ addRoomButtons }
{ settingsButton }
</div>
{ previewRooms }
</div>;
};
const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const numFields = 3;
const placeholders = [_t("General"), _t("Random"), _t("Support")];
// TODO vary default prefills for "Just Me" spaces
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
const fields = new Array(numFields).fill(0).map((_, i) => {
const name = "roomName" + i;
return <Field
key={name}
name={name}
type="text"
label={_t("Room name")}
placeholder={placeholders[i]}
value={roomNames[i]}
onChange={ev => setRoomName(i, ev.target.value)}
/>;
});
const onNextClick = async () => {
setError("");
setBusy(true);
try {
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => {
return createRoom({
createOpts: {
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
name,
},
spinner: false,
encryption: false,
andView: false,
inlineErrors: true,
parentSpace: space,
});
}));
onFinished();
} catch (e) {
console.error("Failed to create initial space rooms", e);
setError(_t("Failed to create initial space rooms"));
}
setBusy(false);
};
let onClick = onFinished;
let buttonLabel = _t("Skip for now");
if (roomNames.some(name => name.trim())) {
onClick = onNextClick;
buttonLabel = busy ? _t("Creating rooms...") : _t("Next")
}
return <div>
<h1>{ title }</h1>
<div className="mx_SpaceRoomView_description">{ description }</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
{ fields }
<div className="mx_SpaceRoomView_buttons">
<FormButton
label={buttonLabel}
disabled={busy}
onClick={onClick}
/>
</div>
</div>;
};
const SpaceSetupPublicShare = ({ space, onFinished }) => {
return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share your public space") }</h1>
<div className="mx_SpacePublicShare_description">{ _t("At the moment only you can see it.") }</div>
<SpacePublicShare space={space} onFinished={onFinished} />
<div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Finish")} onClick={onFinished} />
</div>
</div>;
};
const SpaceSetupPrivateScope = ({ onFinished }) => {
const [option, setOption] = useState<string>(null);
return <div className="mx_SpaceRoomView_privateScope">
<h1>{ _t("Who are you working with?") }</h1>
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
<StyledRadioGroup
name="privateSpaceScope"
value={option}
onChange={setOption}
definitions={[
{
value: "justMe",
className: "mx_SpaceRoomView_privateScope_justMeButton",
label: <React.Fragment>
<h3>{ _t("Just Me") }</h3>
<div>{ _t("A private space just for you") }</div>
</React.Fragment>,
}, {
value: "meAndMyTeammates",
className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton",
label: <React.Fragment>
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</React.Fragment>,
},
]}
/>
<div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Next")} disabled={!option} onClick={() => onFinished(option !== "justMe")} />
</div>
</div>;
};
const validateEmailRules = withValidation({
rules: [{
key: "email",
test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t("Doesn't look like a valid email address"),
}],
});
const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const numFields = 3;
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
const fields = new Array(numFields).fill(0).map((_, i) => {
const name = "emailAddress" + i;
return <Field
key={name}
name={name}
type="text"
label={_t("Email address")}
placeholder={_t("Email")}
value={emailAddresses[i]}
onChange={ev => setEmailAddress(i, ev.target.value)}
ref={fieldRefs[i]}
onValidate={validateEmailRules}
/>;
});
const onNextClick = async () => {
setError("");
for (let i = 0; i < fieldRefs.length; i++) {
const fieldRef = fieldRefs[i];
const valid = await fieldRef.current.validate({ allowEmpty: true });
if (valid === false) { // true/null are allowed
fieldRef.current.focus();
fieldRef.current.validate({ allowEmpty: true, focused: true });
return;
}
}
setBusy(true);
const targetIds = emailAddresses.map(name => name.trim()).filter(Boolean);
try {
const result = await inviteMultipleToRoom(space.roomId, targetIds);
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error");
if (failedUsers.length > 0) {
console.log("Failed to invite users to space: ", result);
setError(_t("Failed to invite the following users to your space: %(csvUsers)s", {
csvUsers: failedUsers.join(", "),
}));
} else {
onFinished();
}
} catch (err) {
console.error("Failed to invite users to space: ", err);
setError(_t("We couldn't invite those users. Please check the users you want to invite and try again."));
}
setBusy(false);
};
return <div className="mx_SpaceRoomView_inviteTeammates">
<h1>{ _t("Invite your teammates") }</h1>
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
{ fields }
<div className="mx_SpaceRoomView_inviteTeammates_buttons">
<AccessibleButton
className="mx_SpaceRoomView_inviteTeammates_inviteDialogButton"
onClick={() => showRoomInviteDialog(space.roomId)}
>
{ _t("Invite by username") }
</AccessibleButton>
</div>
<div className="mx_SpaceRoomView_buttons">
<AccessibleButton onClick={onFinished} kind="link">{_t("Skip for now")}</AccessibleButton>
<FormButton label={busy ? _t("Inviting...") : _t("Next")} disabled={busy} onClick={onNextClick} />
</div>
</div>;
};
export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
static contextType = MatrixClientContext;
private readonly creator: string;
private readonly dispatcherRef: string;
private readonly rightPanelStoreToken: EventSubscription;
constructor(props, context) {
super(props, context);
let phase = Phase.Landing;
this.creator = this.props.space.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
const showSetup = this.props.justCreatedOpts && this.context.getUserId() === this.creator;
if (showSetup) {
phase = this.props.justCreatedOpts.createOpts.preset === Preset.PublicChat
? Phase.PublicCreateRooms : Phase.PrivateScope;
}
this.state = {
phase,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
}
componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
this.rightPanelStoreToken.remove();
}
private onRightPanelStoreUpdate = () => {
this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
});
};
private onAction = (payload: ActionPayload) => {
if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return;
if (payload.action === Action.ViewUser && payload.member) {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.SpaceMemberInfo,
refireParams: {
space: this.props.space,
member: payload.member,
},
});
} else if (payload.action === "view_3pid_invite" && payload.event) {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.Space3pidMemberInfo,
refireParams: {
space: this.props.space,
event: payload.event,
},
});
} else {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.SpaceMemberList,
refireParams: { space: this.props.space },
});
}
};
private renderBody() {
switch (this.state.phase) {
case Phase.Landing:
return <SpaceLanding
space={this.props.space}
onJoinButtonClicked={this.props.onJoinButtonClicked}
onRejectButtonClicked={this.props.onRejectButtonClicked}
/>;
case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms
space={this.props.space}
title={_t("What discussions do you want to have?")}
description={_t("We'll create rooms for each topic.")}
onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>;
case Phase.PublicShare:
return <SpaceSetupPublicShare
space={this.props.space}
onFinished={() => this.setState({ phase: Phase.Landing })}
/>;
case Phase.PrivateScope:
return <SpaceSetupPrivateScope
onFinished={(invite: boolean) => {
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
}}
/>;
case Phase.PrivateInvite:
return <SpaceSetupPrivateInvite
space={this.props.space}
onFinished={() => this.setState({ phase: Phase.PrivateCreateRooms })}
/>;
case Phase.PrivateCreateRooms:
return <SpaceSetupFirstRooms
space={this.props.space}
title={_t("What projects are you working on?")}
description={_t("We'll create rooms for each of them. You can add existing rooms after setup.")}
onFinished={() => this.setState({ phase: Phase.Landing })}
/>;
}
}
render() {
const rightPanel = this.state.showRightPanel && this.state.phase === Phase.Landing
? <RightPanel room={this.props.space} resizeNotifier={this.props.resizeNotifier} />
: null;
return <main className="mx_SpaceRoomView">
<ErrorBoundary>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
{ this.renderBody() }
</MainSplit>
</ErrorBoundary>
</main>;
}
}

View file

@ -15,8 +15,13 @@ limitations under the License.
*/
import React, { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import * as fbEmitter from "fbemitter";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
import dis from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
@ -34,7 +39,6 @@ import {getHomePageUrl} from "../../utils/pages";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from '../views/avatars/BaseAvatar';
import classNames from "classnames";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { SettingLevel } from "../../settings/SettingLevel";
import IconizedContextMenu, {
@ -42,16 +46,16 @@ import IconizedContextMenu, {
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import * as fbEmitter from "fbemitter";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import { showCommunityInviteDialog } from "../../RoomInvite";
import dis from "../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName";
interface IProps {
isMinimized: boolean;
@ -62,6 +66,7 @@ type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
interface IState {
contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean;
selectedSpace?: Room;
}
export default class UserMenu extends React.Component<IProps, IState> {
@ -79,6 +84,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
if (SettingsStore.getValue("feature_spaces")) {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
}
private get hasHomePage(): boolean {
@ -96,6 +104,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef.remove();
if (SettingsStore.getValue("feature_spaces")) {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
}
private onTagStoreUpdate = () => {
@ -120,6 +131,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.forceUpdate();
};
private onSelectedSpaceUpdate = async (selectedSpace?: Room) => {
this.setState({ selectedSpace });
};
private onThemeChanged = () => {
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
};
@ -517,7 +532,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
{/* masked image in CSS */}
</span>
);
if (prototypeCommunityName) {
if (this.state.selectedSpace) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{displayName}</span>
<RoomName room={this.state.selectedSpace}>
{(roomName) => <span className="mx_UserMenu_subUserName">{roomName}</span>}
</RoomName>
</div>
);
} else if (prototypeCommunityName) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{prototypeCommunityName}</span>

View file

@ -218,6 +218,9 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
'monthly_active_user': _td(
"This homeserver has hit its Monthly Active User limit.",
),
'hs_blocked': _td(
"This homeserver has been blocked by it's administrator.",
),
'': _td(
"This homeserver has exceeded one of its resource limits.",
),

View file

@ -276,6 +276,7 @@ export default class Registration extends React.Component<IProps, IState> {
response.data.admin_contact,
{
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
'hs_blocked': _td("This homeserver has been blocked by it's administrator."),
'': _td("This homeserver has exceeded one of its resource limits."),
},
);

View file

@ -0,0 +1,208 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from "react";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import FormButton from "../elements/FormButton";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import {getDisplayAliasForRoom} from "../../../Rooms";
import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {allSettled} from "../../../utils/promise";
import DMRoomMap from "../../../utils/DMRoomMap";
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
import StyledCheckbox from "../elements/StyledCheckbox";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
space: Room;
onCreateRoomClick(cli: MatrixClient, space: Room): void;
}
const Entry = ({ room, checked, onChange }) => {
return <div className="mx_AddExistingToSpaceDialog_entry">
<RoomAvatar room={room} height={32} width={32} />
<span className="mx_AddExistingToSpaceDialog_entry_name">{ room.name }</span>
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const [selectedSpace, setSelectedSpace] = useState(space);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const existingSubspacesSet = new Set(existingSubspaces);
const spaces = SpaceStore.instance.getSpaces().filter(s => {
return !existingSubspacesSet.has(s) // not already in space
&& space !== s // not the top-level space
&& selectedSpace !== s // not the selected space
&& s.name.toLowerCase().includes(lcQuery); // contains query
});
const existingRooms = SpaceStore.instance.getChildRooms(space.roomId);
const existingRoomsSet = new Set(existingRooms);
const rooms = cli.getVisibleRooms().filter(room => {
return !existingRoomsSet.has(room) // not already in space
&& room.name.toLowerCase().includes(lcQuery) // contains query
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
});
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
let spaceOptionSection;
if (existingSubspacesSet.size > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing spaces/rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
return <BaseDialog
title={title}
className="mx_AddExistingToSpaceDialog"
contentId="mx_AddExistingToSpaceDialog"
onFinished={onFinished}
fixedWidth={false}
>
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
/>
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : undefined }
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
<div className="mx_AddExistingToSpaceDialog_footer">
<span>
<div>{ _t("Don't want to add an existing room?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</span>
<FormButton
label={busy ? _t("Applying...") : _t("Apply")}
disabled={busy || selectedToAdd.size < 1}
onClick={async () => {
setBusy(true);
try {
await allSettled(Array.from(selectedToAdd).map((room) =>
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
onFinished(true);
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space"));
}
setBusy(false);
}}
/>
</div>
</BaseDialog>;
};
export default AddExistingToSpaceDialog;

View file

@ -17,6 +17,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {Room} from "matrix-js-sdk/src/models/room";
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import withValidation from '../elements/Validation';
@ -30,6 +32,7 @@ export default class CreateRoomDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
defaultPublic: PropTypes.bool,
parentSpace: PropTypes.instanceOf(Room),
};
constructor(props) {
@ -85,6 +88,10 @@ export default class CreateRoomDialog extends React.Component {
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
}
if (this.props.parentSpace) {
opts.parentSpace = this.props.parentSpace;
}
return opts;
}

View file

@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
className: PropTypes.string,
title: PropTypes.string,
description: PropTypes.node,
button: PropTypes.string,
button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func,
@ -60,11 +60,11 @@ export default class InfoDialog extends React.Component {
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
{ this.props.description }
</div>
<DialogButtons primaryButton={this.props.button || _t('OK')}
{ this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
onPrimaryButtonClick={this.onFinished}
hasCancel={false}
>
</DialogButtons>
</DialogButtons> }
</BaseDialog>
);
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import {_t} from "../../../languageHandler";
import {_t, _td} from "../../../languageHandler";
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
@ -48,6 +48,7 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
export const KIND_SPACE_INVITE = "space_invite";
export const KIND_CALL_TRANSFER = "call_transfer";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
@ -309,7 +310,7 @@ interface IInviteDialogProps {
// not provided.
kind: string,
// The room ID this dialog is for. Only required for KIND_INVITE.
// The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
roomId: string,
// The call to transfer. Only required for KIND_CALL_TRANSFER.
@ -348,8 +349,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
constructor(props) {
super(props);
if (props.kind === KIND_INVITE && !props.roomId) {
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
}
@ -1026,7 +1027,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
}
if (this.props.kind === KIND_INVITE) {
if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
}
@ -1247,37 +1248,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
buttonText = _t("Go");
goButtonFn = this._startDm;
} else if (this.props.kind === KIND_INVITE) {
title = _t("Invite to this room");
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
let helpTextUntranslated;
if (this.props.kind === KIND_INVITE) {
if (identityServersEnabled) {
helpText = _t(
"Invite someone using their name, email address, username (like <userId/>) or " +
"<a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
{sub}
</a>,
},
);
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
"(like <userId/>) or <a>share this room</a>.");
} else {
helpText = _t(
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
{},
{
helpTextUntranslated = _td("Invite someone using their name, username " +
"(like <userId/>) or <a>share this room</a>.");
}
} else { // KIND_SPACE_INVITE
if (identityServersEnabled) {
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
"(like <userId/>) or <a>share this space</a>.");
} else {
helpTextUntranslated = _td("Invite someone using their name, username " +
"(like <userId/>) or <a>share this space</a>.");
}
}
helpText = _t(helpTextUntranslated, {}, {
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
{sub}
</a>,
},
);
}
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
});
buttonText = _t("Invite");
goButtonFn = this._inviteUsers;

View file

@ -0,0 +1,162 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from 'react';
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {EventType} from "matrix-js-sdk/src/@types/event";
import {_t} from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DevtoolsDialog from "./DevtoolsDialog";
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
import {getTopic} from "../elements/RoomTopic";
import {avatarUrlForRoom} from "../../../Avatar";
import ToggleSwitch from "../elements/ToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import FormButton from "../elements/FormButton";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {allSettled} from "../../../utils/promise";
import {useDispatcher} from "../../../hooks/useDispatcher";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
space: Room;
}
const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFinished }) => {
useDispatcher(defaultDispatcher, ({action, ...params}) => {
if (action === "after_leave_room" && params.room_id === space.roomId) {
onFinished(false);
}
});
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const userId = cli.getUserId();
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
const avatarChanged = newAvatar !== null;
const [name, setName] = useState<string>(space.name);
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
const nameChanged = name !== space.name;
const currentTopic = getTopic(space);
const [topic, setTopic] = useState<string>(currentTopic);
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
const topicChanged = topic !== currentTopic;
const currentJoinRule = space.getJoinRule();
const [joinRule, setJoinRule] = useState(currentJoinRule);
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
const joinRuleChanged = joinRule !== currentJoinRule;
const onSave = async () => {
setBusy(true);
const promises = [];
if (avatarChanged) {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
url: await cli.uploadContent(newAvatar),
}, ""));
}
if (nameChanged) {
promises.push(cli.setRoomName(space.roomId, name));
}
if (topicChanged) {
promises.push(cli.setRoomTopic(space.roomId, topic));
}
if (joinRuleChanged) {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
}
const results = await allSettled(promises);
setBusy(false);
const failures = results.filter(r => r.status === "rejected");
if (failures.length > 0) {
console.error("Failed to save space settings: ", failures);
setError(_t("Failed to save space settings."));
}
};
return <BaseDialog
title={_t("Space settings")}
className="mx_SpaceSettingsDialog"
contentId="mx_SpaceSettingsDialog"
onFinished={onFinished}
fixedWidth={false}
>
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
<div>{ _t("Edit settings relating to your space.") }</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
avatarDisabled={!canSetAvatar}
setAvatar={setNewAvatar}
name={name}
nameDisabled={!canSetName}
setName={setName}
topic={topic}
topicDisabled={!canSetTopic}
setTopic={setTopic}
/>
<div>
{ _t("Make this space private") }
<ToggleSwitch
checked={joinRule === "private"}
onChange={checked => setJoinRule(checked ? "private" : "invite")}
disabled={!canSetJoinRule}
aria-label={_t("Make this space private")}
/>
</div>
<FormButton
kind="danger"
label={_t("Leave Space")}
onClick={() => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: space.roomId,
});
}}
/>
<div className="mx_SpaceSettingsDialog_buttons">
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
{ _t("View dev tools") }
</AccessibleButton>
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
{ _t("Cancel") }
</AccessibleButton>
<FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} />
</div>
</div>
</BaseDialog>;
};
export default SpaceSettingsDialog;

View file

@ -31,6 +31,7 @@ export default class PersistentApp extends React.Component {
componentDidMount() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
}
componentWillUnmount() {
@ -38,6 +39,9 @@ export default class PersistentApp extends React.Component {
this._roomStoreToken.remove();
}
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
}
}
_onRoomViewStoreUpdate = payload => {
@ -53,16 +57,28 @@ export default class PersistentApp extends React.Component {
});
};
_onMyMembership = async (room, membership) => {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") {
// we're not in the room anymore - delete
if (room.roomId === persistentWidgetInRoomId) {
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
}
}
};
render() {
if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (this.state.roomId !== persistentWidgetInRoomId) {
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
// Sanity check the room - the widget may have been destroyed between render cycles, and
// thus no room is associated anymore.
if (!persistentWidgetInRoom) return null;
const myMembership = persistentWidgetInRoom.getMyMembership();
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
// get the widget data
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();

View file

@ -0,0 +1,126 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import Dropdown from "../../views/elements/Dropdown"
import * as sdk from '../../../index';
import PlatformPeg from "../../../PlatformPeg";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
if (language.value.toUpperCase() === query.toUpperCase()) return true;
return false;
}
interface SpellCheckLanguagesDropdownIProps {
className: string,
value: string,
onOptionChange(language: string),
}
interface SpellCheckLanguagesDropdownIState {
searchQuery: string,
languages: any,
}
export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
SpellCheckLanguagesDropdownIState> {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = {
searchQuery: '',
languages: null,
};
}
componentDidMount() {
const plaf = PlatformPeg.get();
if (plaf) {
plaf.getAvailableSpellCheckLanguages().then((languages) => {
languages.sort(function(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
const langs = [];
languages.forEach((language) => {
langs.push({
label: language,
value: language,
})
})
this.setState({languages: langs});
}).catch((e) => {
this.setState({languages: ['en']});
});
}
}
_onSearchChange(search) {
this.setState({
searchQuery: search,
});
}
render() {
if (this.state.languages === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />;
}
let displayedLanguages;
if (this.state.searchQuery) {
displayedLanguages = this.state.languages.filter((lang) => {
return languageMatchesSearchQuery(this.state.searchQuery, lang);
});
} else {
displayedLanguages = this.state.languages;
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>
{ language.label }
</div>;
});
// default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propgating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let value = null;
if (language) {
value = this.props.value || language;
} else {
language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
}
return <Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
onSearchChange={this._onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}>
{ options }
</Dropdown>;
}
}

View file

@ -62,6 +62,8 @@ import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
import InfoDialog from "../dialogs/InfoDialog";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
interface IDevice {
deviceId: string;
@ -303,7 +305,8 @@ const UserOptionsSection: React.FC<{
member: RoomMember;
isIgnored: boolean;
canInvite: boolean;
}> = ({member, isIgnored, canInvite}) => {
isSpace?: boolean;
}> = ({member, isIgnored, canInvite, isSpace}) => {
const cli = useContext(MatrixClientContext);
let ignoreButton = null;
@ -343,7 +346,7 @@ const UserOptionsSection: React.FC<{
</AccessibleButton>
);
if (member.roomId) {
if (member.roomId && !isSpace) {
const onReadReceiptButton = function() {
const room = cli.getRoom(member.roomId);
dis.dispatch({
@ -435,12 +438,16 @@ const UserOptionsSection: React.FC<{
);
};
const warnSelfDemote = async () => {
const warnSelfDemote = async (isSpace) => {
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
title: _t("Demote yourself?"),
description:
<div>
{ _t("You will not be able to undo this change as you are demoting yourself, " +
{ isSpace
? _t("You will not be able to undo this change as you are demoting yourself, " +
"if you are the last privileged user in the space it will be impossible " +
"to regain privileges.")
: _t("You will not be able to undo this change as you are demoting yourself, " +
"if you are the last privileged user in the room it will be impossible " +
"to regain privileges.") }
</div>,
@ -718,7 +725,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels,
// if muting self, warn as it may be irreversible
if (target === cli.getUserId()) {
try {
if (!(await warnSelfDemote())) return;
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
return;
@ -807,7 +814,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
}
if (me.powerLevel >= redactPowerLevel) {
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
redactButton = (
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
);
@ -1086,7 +1093,7 @@ const PowerLevelEditor: React.FC<{
} else if (myUserId === target) {
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
try {
if (!(await warnSelfDemote())) return;
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
}
@ -1316,12 +1323,10 @@ const BasicUserInfo: React.FC<{
if (!isRoomEncrypted) {
if (!cryptoEnabled) {
text = _t("This client does not support end-to-end encryption.");
} else if (room) {
} else if (room && !room.isSpaceRoom()) {
text = _t("Messages in this room are not end-to-end encrypted.");
} else {
// TODO what to render for GroupMember
}
} else {
} else if (!room.isSpaceRoom()) {
text = _t("Messages in this room are end-to-end encrypted.");
}
@ -1397,7 +1402,9 @@ const BasicUserInfo: React.FC<{
<UserOptionsSection
canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={member} />
member={member}
isSpace={room?.isSpaceRoom()}
/>
{ adminToolsContainer }
@ -1514,7 +1521,7 @@ interface IProps {
user: Member;
groupId?: string;
room?: Room;
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo;
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
onClose(): void;
}
@ -1558,7 +1565,9 @@ const UserInfo: React.FC<Props> = ({
previousPhase = RightPanelPhases.RoomMemberInfo;
refireParams = {member: member};
} else if (room) {
previousPhase = RightPanelPhases.RoomMemberList;
previousPhase = previousPhase = room.isSpaceRoom()
? RightPanelPhases.SpaceMemberList
: RightPanelPhases.RoomMemberList;
}
const onEncryptionPanelClose = () => {
@ -1573,6 +1582,7 @@ const UserInfo: React.FC<Props> = ({
switch (phase) {
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.GroupMemberInfo:
case RightPanelPhases.SpaceMemberInfo:
content = (
<BasicUserInfo
room={room}
@ -1603,7 +1613,18 @@ const UserInfo: React.FC<Props> = ({
}
}
const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} />;
let scopeHeader;
if (room?.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} />
</div>;
}
const header = <React.Fragment>
{ scopeHeader }
<UserInfoHeader member={member} e2eStatus={e2eStatus} />
</React.Fragment>;
return <BaseCard
className={classes.join(" ")}
header={header}

View file

@ -100,10 +100,11 @@ export default class RoomProfileSettings extends React.Component {
const newState = {};
// TODO: What do we do about errors?
const displayName = this.state.displayName.trim();
if (this.state.originalDisplayName !== this.state.displayName) {
await client.setRoomName(this.props.roomId, this.state.displayName);
newState.originalDisplayName = this.state.displayName;
await client.setRoomName(this.props.roomId, displayName);
newState.originalDisplayName = displayName;
newState.displayName = displayName;
}
if (this.state.avatarFile) {

View file

@ -17,10 +17,8 @@ limitations under the License.
import React from 'react';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { Room } from 'matrix-js-sdk/src/models/room'
import * as sdk from '../../../index';
import dis from "../../../dispatcher/dispatcher";
import AppsDrawer from './AppsDrawer';
import { _t } from '../../../languageHandler';
import classNames from 'classnames';
import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";
@ -36,9 +34,6 @@ interface IProps {
userId: string,
showApps: boolean, // Render apps
// set to true to show the file drop target
draggingFile: boolean,
// maxHeight attribute for the aux panel and the video
// therein
maxHeight: number,
@ -149,21 +144,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
}
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let fileDropTarget = null;
if (this.props.draggingFile) {
fileDropTarget = (
<div className="mx_RoomView_fileDropTarget">
<div className="mx_RoomView_fileDropTargetLabel" title={_t("Drop File Here")}>
<TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />
<br />
{ _t("Drop file here to upload") }
</div>
</div>
);
}
const callView = (
<CallViewForRoom
roomId={this.props.room.roomId}
@ -246,7 +226,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
<AutoHideScrollbar className={classes} style={style} >
{ stateViews }
{ appsDrawer }
{ fileDropTarget }
{ callView }
{ this.props.children }
</AutoHideScrollbar>

View file

@ -952,9 +952,6 @@ export default class EventTile extends React.Component {
return (
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
{ ircTimestamp }
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ sender }
{ ircPadlock }
<div className="mx_EventTile_line">
@ -973,6 +970,9 @@ export default class EventTile extends React.Component {
{ reactionsRow }
{ actionBar }
</div>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{
// The avatar goes after the event tile as it's absolutely positioned to be over the
// event tile line, so needs to be later in the DOM so it appears on top (this avoids

View file

@ -27,6 +27,8 @@ import * as sdk from "../../../index";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import BaseCard from "../right_panel/BaseCard";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@ -456,6 +458,8 @@ export default class MemberList extends React.Component {
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat && chat.roomId === this.props.roomId) {
inviteButtonText = _t("Invite to this community");
} else if (room.isSpaceRoom()) {
inviteButtonText = _t("Invite to this space");
}
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@ -483,12 +487,26 @@ export default class MemberList extends React.Component {
onSearch={ this.onSearchQueryChanged } />
);
let previousPhase = RightPanelPhases.RoomSummary;
// We have no previousPhase for when viewing a MemberList from a Space
let scopeHeader;
if (room?.isSpaceRoom()) {
previousPhase = undefined;
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} />
</div>;
}
return <BaseCard
className="mx_MemberList"
header={inviteButton}
header={<React.Fragment>
{ scopeHeader }
{ inviteButton }
</React.Fragment>}
footer={footer}
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
previousPhase={previousPhase}
>
<div className="mx_MemberList_wrapper">
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}

View file

@ -1,7 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2015-2018, 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -19,7 +17,6 @@ import React, {createRef} from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
@ -33,11 +30,8 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview";
import {UIFeature} from "../../../settings/UIFeature";
import WidgetStore from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import { PlaceCallType } from "../../../CallHandler";
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -50,95 +44,18 @@ ComposerAvatar.propTypes = {
me: PropTypes.object.isRequired,
};
function CallButton(props) {
const onVoiceCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
type: PlaceCallType.Voice,
room_id: props.roomId,
});
};
return (<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_voicecall"
onClick={onVoiceCallClick}
title={_t('Voice call')}
/>);
}
CallButton.propTypes = {
roomId: PropTypes.string.isRequired,
};
function VideoCallButton(props) {
const onCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video,
room_id: props.roomId,
});
};
return <AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_videocall"
onClick={onCallClick}
title={_t('Video call')}
/>;
}
VideoCallButton.propTypes = {
roomId: PropTypes.string.isRequired,
};
function HangupButton(props) {
const onHangupClick = () => {
if (props.isConference) {
dis.dispatch({
action: props.canEndConference ? 'end_conference' : 'hangup_conference',
room_id: props.roomId,
});
return;
}
const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
if (!call) {
return;
}
const action = call.state === CallState.Ringing ? 'reject' : 'hangup';
dis.dispatch({
action,
// hangup the call for this room. NB. We use the room in props as the room ID
// as call.roomId may be the 'virtual room', and the dispatch actions always
// use the user-facing room (there was a time when we deliberately used
// call.roomId and *not* props.roomId, but that was for the old
// style Freeswitch conference calls and those times are gone.)
room_id: props.roomId,
});
};
let tooltip = _t("Hangup");
if (props.isConference && props.canEndConference) {
tooltip = _t("End conference");
}
const canLeaveConference = !props.isConference ? true : props.isInConference;
function SendButton(props) {
return (
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_hangup"
onClick={onHangupClick}
title={tooltip}
disabled={!canLeaveConference}
className="mx_MessageComposer_sendMessage"
onClick={props.onClick}
title={_t('Send message')}
/>
);
}
HangupButton.propTypes = {
roomId: PropTypes.string.isRequired,
isConference: PropTypes.bool.isRequired,
canEndConference: PropTypes.bool,
isInConference: PropTypes.bool,
SendButton.propTypes = {
onClick: PropTypes.func.isRequired,
};
const EmojiButton = ({addEmoji}) => {
@ -265,9 +182,9 @@ export default class MessageComposer extends React.Component {
this.state = {
tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(),
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
isComposerEmpty: true,
};
}
@ -396,6 +313,16 @@ export default class MessageComposer extends React.Component {
});
}
sendMessage = () => {
this.messageComposerInput._sendMessage();
}
onChange = (model) => {
this.setState({
isComposerEmpty: model.isEmpty,
});
}
render() {
const controls = [
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
@ -405,12 +332,7 @@ export default class MessageComposer extends React.Component {
];
if (!this.state.tombstone && this.state.canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer");
const callInProgress = this.props.callState && this.props.callState !== 'ended';
controls.push(
<SendMessageComposer
@ -421,6 +343,7 @@ export default class MessageComposer extends React.Component {
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.props.permalinkCreator}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
/>,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
@ -431,28 +354,10 @@ export default class MessageComposer extends React.Component {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}
if (this.state.showCallButtons) {
if (this.state.hasConference) {
const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
if (!this.state.isComposerEmpty) {
controls.push(
<HangupButton
key="controls_hangup"
roomId={this.props.room.roomId}
isConference={true}
canEndConference={canEndConf}
isInConference={this.state.joinedConference}
/>,
<SendButton key="controls_send" onClick={this.sendMessage} />,
);
} else if (callInProgress) {
controls.push(
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} isConference={false} />,
);
} else {
controls.push(
<CallButton key="controls_call" roomId={this.props.room.roomId} />,
<VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
);
}
}
} else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];

View file

@ -32,7 +32,7 @@ try {
} catch (e) {
}
export default class ReadReceiptMarker extends React.Component {
export default class ReadReceiptMarker extends React.PureComponent {
static propTypes = {
// the RoomMember to show the RR for
member: PropTypes.object,
@ -155,7 +155,15 @@ export default class ReadReceiptMarker extends React.Component {
// then shift to the rightmost column,
// and then it will drop down to its resting position
startStyles.push({ top: startTopOffset+'px', left: '0px' });
//
// XXX: We use a fractional left value to trick velocity-animate into actually animating.
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
// skip applying it, thus making our read receipt at +14px instead of +0px like it
// should be. This does cause a tiny amount of drift for read receipts, however with a
// value so small it's not perceived by a user.
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
// fail to fall down or cause gaps.
startStyles.push({ top: startTopOffset+'px', left: '0.001px' });
enterTransitionOpts.push({
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',

View file

@ -31,6 +31,7 @@ import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import {PlaceCallType} from "../../../CallHandler";
export default class RoomHeader extends React.Component {
static propTypes = {
@ -45,6 +46,7 @@ export default class RoomHeader extends React.Component {
e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func,
appsShown: PropTypes.bool,
onCallPlaced: PropTypes.func, // (PlaceCallType) => void;
};
static defaultProps = {
@ -226,8 +228,26 @@ export default class RoomHeader extends React.Component {
title={_t("Search")} />;
}
let voiceCallButton;
let videoCallButton;
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
voiceCallButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
title={_t("Voice call")} />;
videoCallButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev) => this.props.onCallPlaced(
ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
title={_t("Video call")} />;
}
const rightRow =
<div className="mx_RoomHeader_buttons">
{ videoCallButton }
{ voiceCallButton }
{ pinnedEventsButton }
{ forgetButton }
{ appsButton }

View file

@ -47,6 +47,9 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import CallHandler from "../../../CallHandler";
import SpaceStore from "../../../stores/SpaceStore";
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
import { EventType } from "matrix-js-sdk/src/@types/event";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
@ -152,6 +155,50 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
defaultHidden: false,
addRoomLabel: _td("Add room"),
addRoomContextMenu: (onFinished: () => void) => {
if (SpaceStore.instance.activeSpace) {
const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
MatrixClientPeg.get().getUserId());
return <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
/>
<IconizedContextMenuOption
label={_t("Explore space rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
}
return <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}

View file

@ -117,6 +117,7 @@ export default class SendMessageComposer extends React.Component {
placeholder: PropTypes.string,
permalinkCreator: PropTypes.object.isRequired,
replyToEvent: PropTypes.object,
onChange: PropTypes.func,
};
static contextType = MatrixClientContext;
@ -403,8 +404,10 @@ export default class SendMessageComposer extends React.Component {
this._editorRef.clearUndoHistory();
this._editorRef.focus();
this._clearStoredEditorState();
if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
dis.dispatch({action: "scroll_to_bottom"});
}
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
@ -536,10 +539,15 @@ export default class SendMessageComposer extends React.Component {
}
}
onChange = () => {
if (this.props.onChange) this.props.onChange(this.model);
}
render() {
return (
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this._onKeyDown}>
<BasicMessageComposer
onChange={this.onChange}
ref={this._setEditorRef}
model={this.model}
room={this.props.room}

View file

@ -23,6 +23,8 @@ import dis from "../../../dispatcher/dispatcher";
import * as sdk from "../../../index";
import Modal from "../../../Modal";
import {isValid3pidInvite} from "../../../RoomInvite";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
export default class ThirdPartyMemberInfo extends React.Component {
static propTypes = {
@ -32,14 +34,14 @@ export default class ThirdPartyMemberInfo extends React.Component {
constructor(props) {
super(props);
const room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
const me = room.getMember(MatrixClientPeg.get().getUserId());
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
const me = this.room.getMember(MatrixClientPeg.get().getUserId());
const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", "");
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
if (typeof(kickLevel) !== 'number') kickLevel = 50;
const sender = room.getMember(this.props.event.getSender());
const sender = this.room.getMember(this.props.event.getSender());
this.state = {
stateKey: this.props.event.getStateKey(),
@ -119,9 +121,18 @@ export default class ThirdPartyMemberInfo extends React.Component {
);
}
let scopeHeader;
if (this.room.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={this.room} height={32} width={32} />
<RoomName room={this.room} />
</div>;
}
// We shamelessly rip off the MemberInfo styles here.
return (
<div className="mx_MemberInfo" role="tabpanel">
{ scopeHeader }
<div className="mx_MemberInfo_name">
<AccessibleButton className="mx_MemberInfo_cancel"
onClick={this.onCancel}

View file

@ -81,10 +81,12 @@ export default class ProfileSettings extends React.Component {
const client = MatrixClientPeg.get();
const newState = {};
const displayName = this.state.displayName.trim();
try {
if (this.state.originalDisplayName !== this.state.displayName) {
await client.setDisplayName(this.state.displayName);
newState.originalDisplayName = this.state.displayName;
await client.setDisplayName(displayName);
newState.originalDisplayName = displayName;
newState.displayName = displayName;
}
if (this.state.avatarFile) {

View file

@ -0,0 +1,111 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import {_t} from "../../../languageHandler";
interface ExistingSpellCheckLanguageIProps {
language: string,
onRemoved(language: string),
}
interface SpellCheckLanguagesIProps {
languages: Array<string>,
onLanguagesChange(languages: Array<string>),
}
interface SpellCheckLanguagesIState {
newLanguage: string,
}
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
return this.props.onRemoved(this.props.language);
};
render() {
return (
<div className="mx_ExistingSpellCheckLanguage">
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
{_t("Remove")}
</AccessibleButton>
</div>
);
}
}
export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
constructor(props) {
super(props);
this.state = {
newLanguage: "",
}
}
_onRemoved = (language) => {
const languages = this.props.languages.filter((e) => e !== language);
this.props.onLanguagesChange(languages);
};
_onAddClick = (e) => {
e.stopPropagation();
e.preventDefault();
const language = this.state.newLanguage;
if (!language) return;
if (this.props.languages.includes(language)) return;
this.props.languages.push(language)
this.props.onLanguagesChange(this.props.languages);
};
_onNewLanguageChange = (language: string) => {
if (this.state.newLanguage === language) return;
this.setState({newLanguage: language});
};
render() {
const existingSpellCheckLanguages = this.props.languages.map((e) => {
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
});
const addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
</AccessibleButton>
);
return (
<div className="mx_SpellCheckLanguages">
{existingSpellCheckLanguages}
<form onSubmit={this._onAddClick} noValidate={true}>
<SpellCheckLanguagesDropdown
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
value={this.state.newLanguage}
onOptionChange={this._onNewLanguageChange} />
{addButton}
</form>
</div>
);
}
}

View file

@ -22,6 +22,7 @@ import ProfileSettings from "../../ProfileSettings";
import * as languageHandler from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
import LanguageDropdown from "../../../elements/LanguageDropdown";
import SpellCheckSettings from "../../SpellCheckSettings";
import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types";
@ -49,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
this.state = {
language: languageHandler.getCurrentLanguage(),
spellCheckLanguages: [],
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverSupportsSeparateAddAndBind: null,
idServerHasUnsignedTerms: false,
@ -85,6 +87,15 @@ export default class GeneralUserSettingsTab extends React.Component {
this._getThreepidState();
}
async componentDidMount() {
const plaf = PlatformPeg.get();
if (plaf) {
this.setState({
spellCheckLanguages: await plaf.getSpellCheckLanguages(),
});
}
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
}
@ -182,6 +193,15 @@ export default class GeneralUserSettingsTab extends React.Component {
PlatformPeg.get().reload();
};
_onSpellCheckLanguagesChange = (languages) => {
this.setState({spellCheckLanguages: languages});
const plaf = PlatformPeg.get();
if (plaf) {
plaf.setSpellCheckLanguages(languages);
}
};
_onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || "";
@ -303,6 +323,16 @@ export default class GeneralUserSettingsTab extends React.Component {
);
}
_renderSpellCheckSection() {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
<SpellCheckSettings languages={this.state.spellCheckLanguages}
onLanguagesChange={this._onSpellCheckLanguagesChange} />
</div>
);
}
_renderDiscoverySection() {
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
@ -381,6 +411,9 @@ export default class GeneralUserSettingsTab extends React.Component {
}
render() {
const plaf = PlatformPeg.get();
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
const discoWarning = this.state.requiredPolicyInfo.hasTerms
? <img className='mx_GeneralUserSettingsTab_warningIcon'
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
@ -409,6 +442,7 @@ export default class GeneralUserSettingsTab extends React.Component {
{this._renderProfileSection()}
{this._renderAccountSection()}
{this._renderLanguageSection()}
{supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
{ discoverySection }
{this._renderIntegrationManagerSection() /* Has its own title */}
{ accountManagementSection }

View file

@ -48,6 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showRedactions',
'enableSyntaxHighlightLanguageDetection',
'expandCodeByDefault',
'scrollToBottomOnMessageSent',
'showCodeLineNumbers',
'showJoinLeaves',
'showAvatarChanges',

View file

@ -0,0 +1,120 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useRef, useState} from "react";
import {_t} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
interface IProps {
avatarUrl?: string;
avatarDisabled?: boolean;
name?: string,
nameDisabled?: boolean;
topic?: string;
topicDisabled?: boolean;
setAvatar(avatar: File): void;
setName(name: string): void;
setTopic(topic: string): void;
}
const SpaceBasicSettings = ({
avatarUrl,
avatarDisabled = false,
setAvatar,
name = "",
nameDisabled = false,
setName,
topic = "",
topicDisabled = false,
setTopic,
}: IProps) => {
const avatarUploadRef = useRef<HTMLInputElement>();
const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
let avatarSection;
if (avatarDisabled) {
if (avatar) {
avatarSection = <img className="mx_SpaceBasicSettings_avatar" src={avatar} alt="" />;
} else {
avatarSection = <div className="mx_SpaceBasicSettings_avatar" />;
}
} else {
if (avatar) {
avatarSection = <React.Fragment>
<AccessibleButton
className="mx_SpaceBasicSettings_avatar"
onClick={() => avatarUploadRef.current?.click()}
element="img"
src={avatar}
alt=""
/>
<AccessibleButton onClick={() => {
avatarUploadRef.current.value = "";
setAvatarDataUrl(undefined);
setAvatar(undefined);
}} kind="link" className="mx_SpaceBasicSettings_avatar_remove">
{ _t("Delete") }
</AccessibleButton>
</React.Fragment>;
} else {
avatarSection = <React.Fragment>
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
<AccessibleButton onClick={() => avatarUploadRef.current?.click()} kind="link">
{ _t("Upload") }
</AccessibleButton>
</React.Fragment>;
}
}
return <div className="mx_SpaceBasicSettings">
<div className="mx_SpaceBasicSettings_avatarContainer">
{ avatarSection }
<input type="file" ref={avatarUploadRef} onChange={(e) => {
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/*" />
</div>
<Field
name="spaceName"
label={_t("Name")}
autoFocus={true}
value={name}
onChange={ev => setName(ev.target.value)}
disabled={nameDisabled}
/>
<Field
name="spaceTopic"
element="textarea"
label={_t("Description")}
value={topic}
onChange={ev => setTopic(ev.target.value)}
rows={3}
disabled={topicDisabled}
/>
</div>;
};
export default SpaceBasicSettings;

View file

@ -0,0 +1,175 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext, useState} from "react";
import classNames from "classnames";
import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
import {_t} from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
import FormButton from "../elements/FormButton";
import createRoom, {IStateEvent, Preset} from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import SpaceBasicSettings from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton";
import FocusLock from "react-focus-lock";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
<AccessibleButton className={classNames("mx_SpaceCreateMenuType", className)} onClick={onClick}>
<h3>{ title }</h3>
<span>{ description }</span>
</AccessibleButton>
);
};
enum Visibility {
Public,
Private,
}
const SpaceCreateMenu = ({ onFinished }) => {
const cli = useContext(MatrixClientContext);
const [visibility, setVisibility] = useState<Visibility>(null);
const [name, setName] = useState("");
const [avatar, setAvatar] = useState<File>(null);
const [topic, setTopic] = useState<string>("");
const [busy, setBusy] = useState<boolean>(false);
const onSpaceCreateClick = async () => {
if (busy) return;
setBusy(true);
const initialState: IStateEvent[] = [
{
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 },
});
}
if (topic) {
initialState.push({
type: EventType.RoomTopic,
content: { topic },
});
}
try {
await createRoom({
createOpts: {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
creation_content: {
// Based on MSC1840
[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,
},
},
spinner: false,
encryption: false,
andView: true,
inlineErrors: true,
});
onFinished();
} catch (e) {
console.error(e);
}
};
let body;
if (visibility === null) {
body = <React.Fragment>
<h2>{ _t("Create a space") }</h2>
<p>{ _t("Organise rooms into spaces, for just you or anyone") }</p>
<SpaceCreateMenuType
title={_t("Public")}
description={_t("Open space for anyone, best for communities")}
className="mx_SpaceCreateMenuType_public"
onClick={() => setVisibility(Visibility.Public)}
/>
<SpaceCreateMenuType
title={_t("Private")}
description={_t("Invite only space, best for yourself or teams")}
className="mx_SpaceCreateMenuType_private"
onClick={() => setVisibility(Visibility.Private)}
/>
{/*<p>{ _t("Looking to join an existing space?") }</p>*/}
</React.Fragment>;
} else {
body = <React.Fragment>
<AccessibleTooltipButton
className="mx_SpaceCreateMenu_back"
onClick={() => setVisibility(null)}
title={_t("Go back")}
/>
<h2>
{
visibility === Visibility.Public
? _t("Personalise your public space")
: _t("Personalise your private space")
}
</h2>
<p>
{
_t("Give it a photo, name and description to help you identify it.")
} {
_t("You can change these at any point.")
}
</p>
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
<FormButton
label={busy ? _t("Creating...") : _t("Create")}
onClick={onSpaceCreateClick}
disabled={!name && !busy}
/>
</React.Fragment>;
}
return <ContextMenu
left={72}
top={62}
chevronOffset={0}
chevronFace={ChevronFace.None}
onFinished={onFinished}
wrapperClassName="mx_SpaceCreateMenu_wrapper"
managed={false}
>
<FocusLock returnFocus={true}>
{ body }
</FocusLock>
</ContextMenu>;
}
export default SpaceCreateMenu;

View file

@ -0,0 +1,238 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from "react";
import 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 AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import {
RovingAccessibleButton,
RovingAccessibleTooltipButton,
RovingTabIndexProvider,
} from "../../../accessibility/RovingTabIndex";
import {Key} from "../../../Keyboard";
interface IButtonProps {
space?: Room;
className?: string;
selected?: boolean;
tooltip?: string;
notificationState?: SpaceNotificationState;
isNarrow?: boolean;
onClick(): void;
}
const SpaceButton: React.FC<IButtonProps> = ({
space,
className,
selected,
onClick,
tooltip,
notificationState,
isNarrow,
children,
}) => {
const classes = classNames("mx_SpaceButton", className, {
mx_SpaceButton_active: selected,
mx_SpaceButton_narrow: isNarrow,
});
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
if (space) {
avatar = <RoomAvatar width={32} height={32} room={space} />;
}
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge forceCount={false} notification={notificationState} />
</div>;
}
let button;
if (isNarrow) {
button = (
<RovingAccessibleTooltipButton className={classes} title={tooltip} onClick={onClick} role="treeitem">
<div className="mx_SpaceButton_selectionWrapper">
{ avatar }
{ notifBadge }
{ children }
</div>
</RovingAccessibleTooltipButton>
);
} else {
button = (
<RovingAccessibleButton className={classes} onClick={onClick} role="treeitem">
<div className="mx_SpaceButton_selectionWrapper">
{ avatar }
<span className="mx_SpaceButton_name">{ tooltip }</span>
{ notifBadge }
{ children }
</div>
</RovingAccessibleButton>
);
}
return <li className={classNames({
"mx_SpaceItem": true,
"collapsed": isNarrow,
})}>
{ button }
</li>;
}
const useSpaces = (): [Room[], Room | null] => {
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
return [spaces, activeSpace];
};
const SpacePanel = () => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
const [spaces, activeSpace] = useSpaces();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
const newClasses = classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed,
});
let contextMenu = null;
if (menuDisplayed) {
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
}
const onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
switch (ev.key) {
case Key.ARROW_UP:
onMoveFocus(ev.target as Element, true);
break;
case Key.ARROW_DOWN:
onMoveFocus(ev.target as Element, false);
break;
default:
handled = false;
}
if (handled) {
// consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault();
}
};
const onMoveFocus = (element: Element, up: boolean) => {
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes: DOMTokenList;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
element = up ? element.lastElementChild : element.firstElementChild;
descending = true;
}
classes = element.classList;
}
} while (element && !classes.contains("mx_SpaceButton"));
if (element) {
(element as HTMLElement).focus();
}
};
const activeSpaces = activeSpace ? [activeSpace] : [];
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
// TODO drag and drop for re-arranging order
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({onKeyDownHandler}) => (
<ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
onKeyDown={onKeyDownHandler}
>
<AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
<div className="mx_SpaceTreeLevel">
<SpaceButton
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={!activeSpace}
tooltip={_t("Home")}
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
isNarrow={isPanelCollapsed}
/>
{ spaces.map(s => <SpaceItem
key={s.roomId}
space={s}
activeSpaces={activeSpaces}
isPanelCollapsed={isPanelCollapsed}
onExpand={() => setPanelCollapsed(false)}
/>) }
</div>
<SpaceButton
className={newClasses}
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={menuDisplayed ? closeMenu : openMenu}
isNarrow={isPanelCollapsed}
/>
</AutoHideScrollbar>
<AccessibleTooltipButton
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
title={expandCollapseButtonTitle}
/>
{ contextMenu }
</ul>
)}
</RovingTabIndexProvider>
};
export default SpacePanel;

View file

@ -0,0 +1,65 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from "react";
import {Room} from "matrix-js-sdk/src/models/room";
import {_t} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import {copyPlaintext} from "../../../utils/strings";
import {sleep} from "../../../utils/promise";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {showRoomInviteDialog} from "../../../RoomInvite";
interface IProps {
space: Room;
onFinished(): void;
}
const SpacePublicShare = ({ space, onFinished }: IProps) => {
const [copiedText, setCopiedText] = useState(_t("Click to copy"));
return <div className="mx_SpacePublicShare">
<AccessibleButton
className="mx_SpacePublicShare_shareButton"
onClick={async () => {
const permalinkCreator = new RoomPermalinkCreator(space);
permalinkCreator.load();
const success = await copyPlaintext(permalinkCreator.forRoom());
const text = success ? _t("Copied!") : _t("Failed to copy");
setCopiedText(text);
await sleep(10);
if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
setCopiedText(_t("Click to copy"));
}
}}
>
{ _t("Share invite link") }
<span>{ copiedText }</span>
</AccessibleButton>
<AccessibleButton
className="mx_SpacePublicShare_inviteButton"
onClick={() => {
showRoomInviteDialog(space.roomId);
onFinished();
}}
>
{ _t("Invite by email or username") }
</AccessibleButton>
</div>;
};
export default SpacePublicShare;

View file

@ -0,0 +1,405 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import NotificationBadge from "../rooms/NotificationBadge";
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
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, showCreateNewRoom, showSpaceSettings} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {ButtonEvent} from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
import SpacePublicShare from "./SpacePublicShare";
import {Action} from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {showRoomInviteDialog} from "../../../RoomInvite";
import InfoDialog from "../dialogs/InfoDialog";
import {EventType} from "matrix-js-sdk/src/@types/event";
import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory";
interface IItemProps {
space?: Room;
activeSpaces: Room[];
isNested?: boolean;
isPanelCollapsed?: boolean;
onExpand?: Function;
}
interface IItemState {
collapsed: boolean;
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
}
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
static contextType = MatrixClientContext;
constructor(props) {
super(props);
this.state = {
collapsed: !props.isNested, // default to collapsed for root items
contextMenuPosition: null,
};
}
private toggleCollapse(evt) {
if (this.props.onExpand && this.state.collapsed) {
this.props.onExpand();
}
this.setState({collapsed: !this.state.collapsed});
// don't bubble up so encapsulating button for space
// doesn't get triggered
evt.stopPropagation();
}
private onContextMenu = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({
contextMenuPosition: {
right: ev.clientX,
top: ev.clientY,
height: 0,
},
});
}
private onClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
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 onHomeClick = (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 onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
if (this.props.space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite members"),
description: <React.Fragment>
<span>{ _t("Share your public space") }</span>
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
</React.Fragment>,
fixedWidth: false,
button: false,
className: "mx_SpacePanel_sharePublicSpace",
hasCloseButton: true,
});
} else {
showRoomInviteDialog(this.props.space.roomId);
}
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 onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
if (!RoomViewStore.getRoomId()) {
defaultDispatcher.dispatch({
action: "view_room",
room_id: this.props.space.roomId,
}, true);
}
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
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();
Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, {
space: this.props.space,
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
this.setState({contextMenuPosition: null}); // also close the menu
};
private renderContextMenu(): React.ReactElement {
let contextMenu = null;
if (this.state.contextMenuPosition) {
const userId = this.context.getUserId();
let inviteOption;
if (this.props.space.canInvite(userId)) {
inviteOption = (
<IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton"
iconClassName="mx_SpacePanel_iconInvite"
label={_t("Invite people")}
onClick={this.onInviteClick}
/>
);
}
let settingsOption;
let leaveSection;
if (shouldShowSpaceSettings(this.context, this.props.space)) {
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconSettings"
label={_t("Settings")}
onClick={this.onSettingsClick}
/>
);
} else {
leaveSection = <IconizedContextMenuOptionList red first>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconLeave"
label={_t("Leave space")}
onClick={this.onLeaveClick}
/>
</IconizedContextMenuOptionList>;
}
let newRoomOption;
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
newRoomOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("New room")}
onClick={this.onNewRoomClick}
/>
);
}
contextMenu = <IconizedContextMenu
{...toRightOf(this.state.contextMenuPosition, 0)}
onFinished={this.onMenuClose}
className="mx_SpacePanel_contextMenu"
compact
>
<div className="mx_SpacePanel_contextMenu_header">
{ this.props.space.name }
</div>
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHome"
label={_t("Space Home")}
onClick={this.onHomeClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")}
onClick={this.onMembersClick}
/>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label={_t("Explore rooms")}
onClick={this.onExploreRoomsClick}
/>
{ newRoomOption }
</IconizedContextMenuOptionList>
{ leaveSection }
</IconizedContextMenu>;
}
return (
<React.Fragment>
<ContextMenuTooltipButton
className="mx_SpaceButton_menuButton"
onClick={this.onMenuOpenClick}
title={_t("Space options")}
isExpanded={!!this.state.contextMenuPosition}
/>
{ contextMenu }
</React.Fragment>
);
}
render() {
const {space, activeSpaces, isNested} = this.props;
const forceCollapsed = this.props.isPanelCollapsed;
const isNarrow = this.props.isPanelCollapsed;
const collapsed = this.state.collapsed || forceCollapsed;
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId);
const isActive = activeSpaces.includes(space);
const itemClasses = classNames({
"mx_SpaceItem": true,
"collapsed": collapsed,
"hasSubSpaces": childSpaces && childSpaces.length,
});
const classes = classNames("mx_SpaceButton", {
mx_SpaceButton_active: isActive,
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
mx_SpaceButton_narrow: isNarrow,
});
const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
spaces={childSpaces}
activeSpaces={activeSpaces}
isNested={true}
/> : null;
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge forceCount={false} notification={notificationState} />
</div>;
}
const avatarSize = isNested ? 24 : 32;
const toggleCollapseButton = childSpaces && childSpaces.length ?
<button
className="mx_SpaceButton_toggleCollapse"
onClick={evt => this.toggleCollapse(evt)}
/> : null;
let button;
if (isNarrow) {
button = (
<RovingAccessibleTooltipButton
className={classes}
title={space.name}
onClick={this.onClick}
onContextMenu={this.onContextMenu}
forceHide={!!this.state.contextMenuPosition}
role="treeitem"
>
{ toggleCollapseButton }
<div className="mx_SpaceButton_selectionWrapper">
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
{ notifBadge }
{ this.renderContextMenu() }
</div>
</RovingAccessibleTooltipButton>
);
} else {
button = (
<RovingAccessibleButton
className={classes}
onClick={this.onClick}
onContextMenu={this.onContextMenu}
role="treeitem"
>
{ toggleCollapseButton }
<div className="mx_SpaceButton_selectionWrapper">
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
<span className="mx_SpaceButton_name">{ space.name }</span>
{ notifBadge }
{ this.renderContextMenu() }
</div>
</RovingAccessibleButton>
);
}
return (
<li className={itemClasses}>
{ button }
{ childItems }
</li>
);
}
}
interface ITreeLevelProps {
spaces: Room[];
activeSpaces: Room[];
isNested?: boolean;
}
const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
spaces,
activeSpaces,
isNested,
}) => {
return <ul className="mx_SpaceTreeLevel">
{spaces.map(s => {
return (<SpaceItem
key={s.roomId}
activeSpaces={activeSpaces}
space={s}
isNested={isNested}
/>);
})}
</ul>;
}
export default SpaceTreeLevel;

View file

@ -518,7 +518,7 @@ export default class CallView extends React.Component<IProps, IState> {
}
// if we're fullscreen, we don't want to set a maxHeight on the video element.
const maxVideoHeight = getFullScreenElement() ? null : (
const maxVideoHeight = getFullScreenElement() || !this.props.maxVideoHeight ? null : (
this.props.maxVideoHeight - (HEADER_HEIGHT + BOTTOM_PADDING + BOTTOM_MARGIN_TOP_BOTTOM)
);
contentView = <div className={containerClasses}

View file

@ -43,6 +43,7 @@ const RoomContext = createContext<IState>({
canReply: false,
layout: Layout.Group,
matrixClientIsReady: false,
dragCounter: 0,
});
RoomContext.displayName = "RoomContext";
export default RoomContext;

View file

@ -17,6 +17,7 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
@ -31,6 +32,8 @@ import GroupStore from "./stores/GroupStore";
import CountlyAnalytics from "./CountlyAnalytics";
import { isJoinedOrNearlyJoined } from "./utils/membership";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space";
// we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */
@ -41,7 +44,7 @@ enum Visibility {
Private = "private",
}
enum Preset {
export enum Preset {
PrivateChat = "private_chat",
TrustedPrivateChat = "trusted_private_chat",
PublicChat = "public_chat",
@ -54,7 +57,7 @@ interface Invite3PID {
address: string;
}
interface IStateEvent {
export interface IStateEvent {
type: string;
state_key?: string; // defaults to an empty string
content: object;
@ -75,7 +78,7 @@ interface ICreateOpts {
power_level_content_override?: object;
}
interface IOpts {
export interface IOpts {
dmUserId?: string;
createOpts?: ICreateOpts;
spinner?: boolean;
@ -84,6 +87,7 @@ interface IOpts {
inlineErrors?: boolean;
andView?: boolean;
associatedWithCommunity?: string;
parentSpace?: Room;
}
/**
@ -175,6 +179,16 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
});
}
if (opts.parentSpace) {
opts.createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
opts.createOpts.initial_state.push({
type: EventType.RoomHistoryVisibility,
content: {
"history_visibility": opts.createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
},
});
}
let modal;
if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
@ -189,6 +203,9 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
return Promise.resolve();
}
}).then(() => {
if (opts.parentSpace) {
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], true);
}
if (opts.associatedWithCommunity) {
return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false);
}
@ -197,6 +214,9 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
// room has been created, so we race here with the client knowing that
// the room exists, causing things like
// https://github.com/vector-im/vector-web/issues/1813
// Even if we were to block on the echo, servers tend to split the room
// state over multiple syncs so we can't atomically know when we have the
// entire thing.
if (opts.andView) {
dis.dispatch({
action: 'view_room',
@ -206,6 +226,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
// so we are expecting the room to come down the sync
// stream, if it hasn't already.
joining: true,
justCreatedOpts: opts,
});
}
CountlyAnalytics.instance.trackRoomCreate(startTime, roomId);

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { ActionPayload } from "../payloads";
@ -35,4 +36,5 @@ export interface SetRightPanelPhaseRefireParams {
// XXX: The type for event should 'view_3pid_invite' action's payload
event?: any;
widgetId?: string;
space?: Room;
}

View file

@ -60,6 +60,11 @@ function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
}
}
function parseImage(img: HTMLImageElement, partCreator: PartCreator) {
const { src } = img;
return partCreator.plain(`![${img.alt.replace(/[[\\\]]/g, c => "\\" + c)}](${src})`);
}
function parseCodeBlock(n: HTMLElement, partCreator: PartCreator) {
const parts = [];
let language = "";
@ -102,6 +107,8 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
return parseHeader(n, partCreator);
case "A":
return parseLink(<HTMLAnchorElement>n, partCreator);
case "IMG":
return parseImage(<HTMLImageElement>n, partCreator);
case "BR":
return partCreator.newline();
case "EM":

View file

@ -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 {useState} from "react";
// Hook to simplify managing state of arrays of a common type
export const useStateArray = <T>(initialSize: number, initialState: T | T[]): [T[], (i: number, v: T) => void] => {
const [data, setData] = useState<T[]>(() => {
return Array.isArray(initialState) ? initialState : new Array(initialSize).fill(initialState);
});
return [data, (index: number, value: T) => setData(data => {
const copy = [...data];
copy[index] = value;
return copy;
})]
};

View file

@ -851,7 +851,7 @@
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Съобщението Ви не бе изпратено, защото този сървър е някой от лимитите си. Моля, <a>свържете се с администратора на услугата</a> за да продължите да я използвате.",
"Please <a>contact your service administrator</a> to continue using this service.": "Моля, <a>свържете се с администратора на услугата</a> за да продължите да я използвате.",
"Sorry, your homeserver is too old to participate in this room.": "Съжаляваме, вашият сървър е прекалено стар за да участва в тази стая.",
"Please contact your homeserver administrator.": "Моля, свържете се се със сървърния администратор.",
"Please contact your homeserver administrator.": "Моля, свържете се със сървърния администратор.",
"Legal": "Юридически",
"Unable to connect to Homeserver. Retrying...": "Неуспешно свързване със сървъра. Опитване отново...",
"This room has been replaced and is no longer active.": "Тази стая е била заменена и вече не е активна.",
@ -1612,11 +1612,11 @@
"%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s премахна правилото блокиращо достъпа до стаи отговарящи на %(glob)s",
"%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s премахна правилото блокиращо достъпа до сървъри отговарящи на %(glob)s",
"%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s премахна правилото блокиращо достъпа неща отговарящи на %(glob)s",
"%(senderName)s updated an invalid ban rule": "%(senderName)s обнови невалидно правило за блокиране",
"%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s премахна правилото блокиращо достъпа на потребители отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s премахна правилото блокиращо достъпа до стаи отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s премахна правилото блокиращо достъпа до сървъри отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s премахна правилото блокиращо достъпа неща отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s updated an invalid ban rule": "%(senderName)s промени невалидно правило за блокиране",
"%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s промени правилото блокиращо достъпа на потребители отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s промени правилото блокиращо достъпа до стаи отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s промени правилото блокиращо достъпа до сървъри отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s промени правило блокиращо достъпа неща отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s създаде правило блокиращо достъпа на потребители отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s създаде правило блокиращо достъпа до стаи отговарящи на %(glob)s поради %(reason)s",
"%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s създаде правило блокиращо достъпа до сървъри отговарящи на %(glob)s поради %(reason)s",
@ -2530,5 +2530,306 @@
"%(brand)s Desktop": "%(brand)s Desktop",
"%(brand)s Web": "Уеб версия на %(brand)s",
"Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.": "Въведете адреса на вашия Element Matrix Services сървър. Той или използва ваш собствен домейн или е поддомейн на <a>element.io</a>.",
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Може да използвате опцията за собствен сървър, за да влезете в друг Matrix сървър, чрез указване на адреса му. Това позволява да използвате %(brand)s с Matrix профил съществуващ на друг сървър."
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Може да използвате опцията за собствен сървър, за да влезете в друг Matrix сървър, чрез указване на адреса му. Това позволява да използвате %(brand)s с Matrix профил съществуващ на друг сървър.",
"Forgot password?": "Забравена парола?",
"Search (must be enabled)": "Търсене (трябва да е включено)",
"Go to Home View": "Отиване на начален изглед",
"%(creator)s created this DM.": "%(creator)s създаде този директен чат.",
"You have no visible notifications.": "Нямате видими уведомления.",
"Filter rooms and people": "Филтриране на стаи и хора",
"Got an account? <a>Sign in</a>": "Имате профил? <a>Влезте от тук</a>",
"New here? <a>Create an account</a>": "Вие сте нов тук? <a>Създайте профил</a>",
"There was a problem communicating with the homeserver, please try again later.": "Възникна проблем при комуникацията със Home сървъра, моля опитайте отново по-късно.",
"New? <a>Create account</a>": "Вие сте нов? <a>Създайте профил</a>",
"That username already exists, please try another.": "Това потребителско име е вече заето, моля опитайте с друго.",
"Continue with %(ssoButtons)s": "Продължаване с %(ssoButtons)s",
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Или %(usernamePassword)s",
"Already have an account? <a>Sign in here</a>": "Вече имате профил? <a>Влезте от тук</a>",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Ние ще запазим шифровано копие на вашите ключове на нашият сървър. Защитете вашето резервно копие с фраза за сигурност.",
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Ключа за сигурност беше <b>копиран в клиборда</b>, поставете го в:",
"This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Тази сесия откри, че вашата фраза за сигурност и ключ за защитени съобщения бяха премахнати.",
"A new Security Phrase and key for Secure Messages have been detected.": "Новa фраза за сигурност и ключ за защитени съобщения бяха открити.",
"Use Security Key or Phrase": "Използване на ключ или фраза за сигурност",
"Use Security Key": "Използване на ключ за сигурност",
"Great! This Security Phrase looks strong enough.": "Чудесно! Тази фраза за сигурност изглежда достатъчно силна.",
"Set up with a Security Key": "Настройка със ключ за сигурност",
"Please enter your Security Phrase a second time to confirm.": "Моля въведете фразата си за сигурност повторно за да потвърдите.",
"Repeat your Security Phrase...": "Повторете фразата си за сигурност...",
"Your Security Key is in your <b>Downloads</b> folder.": "Ключа за сигурност е във вашата папка <b>Изтегляния</b>.",
"Your Security Key": "Вашият ключ за сигурност",
"Secure your backup with a Security Phrase": "Защитете вашето резервно копие с фраза за сигурност",
"Make a copy of your Security Key": "Направете копие на вашият ключ за сигурност",
"Confirm your Security Phrase": "Потвърдете вашата фраза за сигурност",
"Converts the DM to a room": "Превръща директния чат в стая",
"Converts the room to a DM": "Превръща стаята в директен чат",
"Takes the call in the current room off hold": "Възстановява повикването в текущата стая",
"Places the call in the current room on hold": "Задържа повикването в текущата стая",
"Permission is granted to use the webcam": "Разрешение за използване на уеб камерата е дадено",
"Call failed because webcam or microphone could not be accessed. Check that:": "Неуспешно повикване поради неуспешен достъп до уеб камера или микрофон. Проверете дали:",
"A microphone and webcam are plugged in and set up correctly": "Микрофон и уеб камера са включени и настроени правилно",
"We couldn't log you in": "Не можахме да ви впишем",
"You've reached the maximum number of simultaneous calls.": "Достигнахте максималният брой едновременни повиквания.",
"No other application is using the webcam": "Никое друго приложение не използва уеб камерата",
"Unable to access webcam / microphone": "Неуспешен достъп до уеб камера / микрофон",
"Unable to access microphone": "Неуспешен достъп до микрофон",
"Failed to connect to your homeserver. Please close this dialog and try again.": "Неуспешно свързване към вашият Home сървър. Моля затворете този прозорец и опитайте отново.",
"Abort": "Прекрати",
"%(hostSignupBrand)s Setup": "Настройка на %(hostSignupBrand)s",
"Maximize dialog": "Максимизирай прозореца",
"Minimize dialog": "Минимизирай прозореца",
"Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Научете повече в нашите <privacyPolicyLink />, <termsOfServiceLink /> и <cookiePolicyLink />.",
"Cookie Policy": "Политика за бисквитките",
"Privacy Policy": "Политика за поверителност",
"Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message": "Добавя ┬──┬ ( ゜-゜ノ) в началото на съобщението",
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Добавя (╯°□°)╯︵ ┻━┻ в началото на съобщението",
"Effects": "Ефекти",
"Anguilla": "Ангила",
"British Indian Ocean Territory": "Британска територия в Индийския океан",
"Pitcairn Islands": "острови Питкерн",
"Heard & McDonald Islands": "острови Хърд и Макдоналд",
"Cook Islands": "острови Кук",
"Christmas Island": "остров Рождество",
"Brunei": "Бруней Даруссалам",
"Bouvet Island": "остров Буве",
"Zimbabwe": "Зимбабве",
"Zambia": "Замбия",
"Yemen": "Йемен",
"Western Sahara": "Западна Сахара",
"Wallis & Futuna": "Уолис и Футуна",
"Vietnam": "Виетнам",
"Venezuela": "Венецуела",
"Vatican City": "Ватикан",
"Vanuatu": "Вануату",
"Uzbekistan": "Узбекистан",
"Uruguay": "Уругвай",
"United Arab Emirates": "Обединени арабски емирства",
"Ukraine": "Украйна",
"Uganda": "Уганда",
"U.S. Virgin Islands": "Американски Вирджински острови",
"Tuvalu": "Тувалу",
"Turks & Caicos Islands": "острови Търкс и Кайкос",
"Turkmenistan": "Туркменистан",
"Turkey": "Турция",
"Tunisia": "Тунис",
"Trinidad & Tobago": "Тринидад и Тобаго",
"Tonga": "Тонга",
"Timor-Leste": "Източен Тимор",
"Tokelau": "Токелау",
"Togo": "Того",
"Thailand": "Тайланд",
"Tanzania": "Танзания",
"Tajikistan": "Таджикистан",
"Taiwan": "Тайван",
"São Tomé & Príncipe": "Сао Томе и Принсипи",
"Syria": "Сирия",
"Switzerland": "Швейцария",
"Sweden": "Швеция",
"Swaziland": "Есватини",
"Svalbard & Jan Mayen": "Свалбард и Ян Майен",
"Suriname": "Суринам",
"Sudan": "Судан",
"St. Vincent & Grenadines": "Сейнт Винсънт и Гренадини",
"St. Pierre & Miquelon": "Сен Пиер и Микелон",
"St. Martin": "Сен Мартен",
"St. Lucia": "Сейнт Лусия",
"St. Kitts & Nevis": "Сейнт Китс и Невис",
"St. Helena": "Света Елена",
"St. Barthélemy": "Сен Бартелеми",
"Sri Lanka": "Шри Ланка",
"Spain": "Испания",
"South Sudan": "Южен Судан",
"South Korea": "Южна Корея",
"South Georgia & South Sandwich Islands": "Южна Джорджия и Южни Сандвичеви острови",
"South Africa": "Южна Африка",
"Somalia": "Сомалия",
"Solomon Islands": "Соломонови острови",
"Slovenia": "Словения",
"Slovakia": "Словакия",
"Sint Maarten": "Синт Мартен",
"Singapore": "Сингапур",
"Sierra Leone": "Сиера Леоне",
"Seychelles": "Сейшели",
"Serbia": "Сърбия",
"Senegal": "Сенегал",
"Saudi Arabia": "Саудитска Арабия",
"San Marino": "Сан Марино",
"Samoa": "Самоа",
"Réunion": "Реюнион",
"Rwanda": "Руанда",
"Russia": "Русия",
"Romania": "Румъния",
"Qatar": "Катар",
"Puerto Rico": "Пуерто Рико",
"Portugal": "Португалия",
"Poland": "Полша",
"Philippines": "Филипини",
"Peru": "Перу",
"Paraguay": "Парагвай",
"Papua New Guinea": "Папуа-Нова Гвинея",
"Panama": "Панама",
"Palestine": "Палестина",
"Palau": "Палау",
"Pakistan": "Пакистан",
"Oman": "Оман",
"Norway": "Норвегия",
"Northern Mariana Islands": "Северни Мариански острови",
"North Korea": "Северна Корея",
"Norfolk Island": "остров Норфолк",
"Niue": "Ниуе",
"Nigeria": "Нигерия",
"Niger": "Нигер",
"Nicaragua": "Никарагуа",
"New Zealand": "Нова Зеландия",
"New Caledonia": "Нова Каледония",
"Netherlands": "Нидерландия",
"Nepal": "Непал",
"Nauru": "Науру",
"Namibia": "Намибия",
"Myanmar": "Мианмар (Бирма)",
"Mozambique": "Мозамбик",
"Morocco": "Мароко",
"Montserrat": "Монтсерат",
"Montenegro": "Черна гора",
"Mongolia": "Монголия",
"Monaco": "Монако",
"Moldova": "Молдова",
"Micronesia": "Микронезия",
"Mexico": "Мексико",
"Mayotte": "Майот",
"Mauritius": "Мавриций",
"Mauritania": "Мавритания",
"Martinique": "Мартиника",
"Marshall Islands": "Маршалови острови",
"Malta": "Малта",
"Mali": "Мали",
"Maldives": "Малдиви",
"Malaysia": "Малайзия",
"Malawi": "Малави",
"Madagascar": "Мадагаскар",
"Macedonia": "Северна Македония",
"Macau": "Макао",
"Luxembourg": "Люксембург",
"Lithuania": "Литва",
"Liechtenstein": "Лихтенщайн",
"Libya": "Либия",
"Liberia": "Либерия",
"Lesotho": "Лесото",
"Lebanon": "Ливан",
"Latvia": "Латвия",
"Laos": "Лаос",
"Kyrgyzstan": "Киргизстан",
"Kuwait": "Кувейт",
"Kosovo": "Косово",
"Kiribati": "Кирибати",
"Kenya": "Кения",
"Kazakhstan": "Казахстан",
"Jordan": "Йордания",
"Jersey": "Джърси",
"Japan": "Япония",
"Jamaica": "Ямайка",
"Italy": "Италия",
"Israel": "Израел",
"Isle of Man": "остров Ман",
"Ireland": "Ирландия",
"Iraq": "Ирак",
"Iran": "Иран",
"Indonesia": "Индонезия",
"India": "Индия",
"Iceland": "Исландия",
"Hungary": "Унгария",
"Hong Kong": "Хонконг",
"Honduras": "Хондурас",
"Haiti": "Хаити",
"Guyana": "Гаяна",
"Guinea-Bissau": "Гвинея-Бисау",
"Guinea": "Гвинея",
"Guernsey": "Гърнзи",
"Guatemala": "Гватемала",
"Guam": "Гуам",
"Guadeloupe": "Гваделупа",
"Grenada": "Гренада",
"Greenland": "Гренландия",
"Greece": "Гърция",
"Gibraltar": "Гибралтар",
"Ghana": "Гана",
"Germany": "Германия",
"Georgia": "Грузия",
"Gambia": "Гамбия",
"Gabon": "Габон",
"French Southern Territories": "Френски южни територии",
"French Polynesia": "Френска Полинезия",
"French Guiana": "Френска Гвиана",
"France": "Франция",
"Finland": "Финландия",
"Fiji": "Фиджи",
"Faroe Islands": "Фарьорски острови",
"Falkland Islands": "Фолкландски острови",
"Ethiopia": "Етиопия",
"Estonia": "Естония",
"Eritrea": "Еритрея",
"Equatorial Guinea": "Екваториална Гвинея",
"El Salvador": "Салвадор",
"Egypt": "Египет",
"Ecuador": "Еквадор",
"Dominican Republic": "Доминиканска република",
"Dominica": "Доминика",
"Djibouti": "Джибути",
"Denmark": "Дания",
"Côte dIvoire": "Кот д’Ивоар",
"Czech Republic": "Чешка република",
"Cyprus": "Кипър",
"Curaçao": "Кюрасао",
"Cuba": "Куба",
"Croatia": "Хърватия",
"Costa Rica": "Коста Рика",
"Congo - Kinshasa": "Конго (Киншаса)",
"Congo - Brazzaville": "Конго (Бразавил)",
"Comoros": "Коморски острови",
"Colombia": "Колумбия",
"Cocos (Keeling) Islands": "Кокосови острови",
"China": "Китай",
"Chile": "Чили",
"Chad": "Чад",
"Central African Republic": "Централноафриканска република",
"Cayman Islands": "Кайманови острови",
"Caribbean Netherlands": "Карибска Нидерландия",
"Cape Verde": "Кабо Верде",
"Canada": "Канада",
"Cameroon": "Камерун",
"Cambodia": "Камбоджа",
"Burundi": "Бурунди",
"Burkina Faso": "Буркина Фасо",
"Bulgaria": "България",
"British Virgin Islands": "Британски Вирджински острови",
"Brazil": "Бразилия",
"Botswana": "Ботсвана",
"Bosnia": "Босна и Херцеговина",
"Bolivia": "Боливия",
"Bhutan": "Бутан",
"Bermuda": "Бермудски острови",
"Benin": "Бенин",
"Belize": "Белиз",
"Belgium": "Белгия",
"Belarus": "Беларус",
"Barbados": "Барбадос",
"Bangladesh": "Бангладеш",
"Bahrain": "Бахрейн",
"Bahamas": "Бахамски острови",
"Azerbaijan": "Азербайджан",
"Austria": "Австрия",
"Australia": "Австралия",
"Aruba": "Аруба",
"Armenia": "Армения",
"Argentina": "Аржентина",
"Antigua & Barbuda": "Антигуа и Барбуда",
"Antarctica": "Антарктика",
"Angola": "Ангола",
"Andorra": "Андора",
"American Samoa": "Американска Самоа",
"Algeria": "Алжир",
"Albania": "Албания",
"Åland Islands": "Оландски острови",
"Afghanistan": "Афганистан",
"United States": "Съединените щати",
"United Kingdom": "Обединеното кралство"
}

View file

@ -55,7 +55,7 @@
"Custom Server Options": "Vlastní nastavení serveru",
"Add a widget": "Přidat widget",
"Accept": "Přijmout",
"%(targetName)s accepted an invitation.": "%(targetName)s přijal/a pozvání.",
"%(targetName)s accepted an invitation.": "%(targetName)s přijal(a) pozvání.",
"Account": "Účet",
"Access Token:": "Přístupový token:",
"Add": "Přidat",
@ -86,10 +86,10 @@
"Bans user with given id": "Vykáže uživatele s daným id",
"Cannot add any more widgets": "Nelze přidat žádné další widgety",
"Change Password": "Změnit heslo",
"%(senderName)s changed their profile picture.": "%(senderName)s změnil/a svůj profilový obrázek.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s změnil/a název místnosti na %(roomName)s.",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s odstranil/a název místnosti.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s změnil/a téma na „%(topic)s“.",
"%(senderName)s changed their profile picture.": "%(senderName)s změnil(a) svůj profilový obrázek.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s změnil(a) název místnosti na %(roomName)s.",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s odstranil(a) název místnosti.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s změnil(a) téma na „%(topic)s“.",
"Changes your display nickname": "Změní vaši zobrazovanou přezdívku",
"Command error": "Chyba příkazu",
"Commands": "Příkazy",
@ -113,8 +113,8 @@
"Email address": "E-mailová adresa",
"Emoji": "Emoji",
"Enable automatic language detection for syntax highlighting": "Zapnout automatické rozpoznávání jazyků pro zvýrazňování syntaxe",
"%(senderName)s ended the call.": "%(senderName)s ukončil/a hovor.",
"Enter passphrase": "Zadejte heslo",
"%(senderName)s ended the call.": "%(senderName)s ukončil(a) hovor.",
"Enter passphrase": "Zadejte přístupovou frázi",
"Error decrypting attachment": "Chyba při dešifrování přílohy",
"Error: Problem communicating with the given homeserver.": "Chyba: problém v komunikaci s daným domovským serverem.",
"Existing Call": "Probíhající hovor",
@ -136,18 +136,18 @@
"Forget room": "Zapomenout místnost",
"For security, this session has been signed out. Please sign in again.": "Z bezpečnostních důvodů bylo toto přihlášení ukončeno. Přihlašte se prosím znovu.",
"and %(count)s others...|other": "a %(count)s další...",
"%(widgetName)s widget modified by %(senderName)s": "%(senderName)s upravil/a widget %(widgetName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(senderName)s odstranil/a widget %(widgetName)s",
"%(widgetName)s widget added by %(senderName)s": "%(senderName)s přidal/a widget %(widgetName)s",
"%(widgetName)s widget modified by %(senderName)s": "%(senderName)s upravil(a) widget %(widgetName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(senderName)s odstranil(a) widget %(widgetName)s",
"%(widgetName)s widget added by %(senderName)s": "%(senderName)s přidal(a) widget %(widgetName)s",
"Automatically replace plain text Emoji": "Automaticky nahrazovat textové emoji",
"Failed to upload image": "Obrázek se nepodařilo nahrát",
"%(senderName)s answered the call.": "%(senderName)s přijal/a hovor.",
"%(senderName)s answered the call.": "%(senderName)s přijal(a) hovor.",
"Click to mute audio": "Klepněte pro vypnutí zvuku",
"Failed to verify email address: make sure you clicked the link in the email": "E-mailovou adresu se nepodařilo ověřit. Přesvědčte se, že jste klepli na odkaz v e-mailové zprávě",
"Guests cannot join this room even if explicitly invited.": "Hosté nemohou vstoupit do této místnosti, i když jsou přímo pozváni.",
"Homeserver is": "Domovský server je",
"Identity Server is": "Server identity je",
"I have verified my email address": "Ověřil/a jsem svou e-mailovou adresu",
"I have verified my email address": "Ověřil(a) jsem svou e-mailovou adresu",
"Import": "Importovat",
"Import E2E room keys": "Importovat end-to-end klíče místností",
"Incoming call from %(name)s": "Příchozí hovor od %(name)s",
@ -156,12 +156,12 @@
"Incorrect username and/or password.": "Nesprávné uživatelské jméno nebo heslo.",
"Incorrect verification code": "Nesprávný ověřovací kód",
"Invalid Email Address": "Neplatná e-mailová adresa",
"%(senderName)s invited %(targetName)s.": "%(senderName)s pozval/a uživatele %(targetName)s.",
"%(senderName)s invited %(targetName)s.": "%(senderName)s pozval(a) uživatele %(targetName)s.",
"Invites": "Pozvánky",
"Invites user with given id to current room": "Pozve do aktuální místnosti uživatele s daným id",
"Join Room": "Vstoupit do místnosti",
"%(targetName)s joined the room.": "%(targetName)s vstoupil/a do místnosti.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s vykopl/a uživatele %(targetName)s.",
"%(targetName)s joined the room.": "%(targetName)s vstoupil(a) do místnosti.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s vykopl(a) uživatele %(targetName)s.",
"Kick": "Vykopnout",
"Kicks user with given id": "Vykopne uživatele s daným id",
"Last seen": "Naposledy aktivní",
@ -184,7 +184,7 @@
"Passwords can't be empty": "Hesla nemohou být prázdná",
"Permissions": "Oprávnění",
"Phone": "Telefon",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s změnil/a úroveň oprávnění o %(powerLevelDiffText)s.",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s změnil(a) úroveň oprávnění o %(powerLevelDiffText)s.",
"Define the power level of a user": "Stanovte úroveň oprávnění uživatele",
"Failed to change power level": "Nepodařilo se změnit úroveň oprávnění",
"Power level must be positive integer.": "Úroveň oprávnění musí být kladné celé číslo.",
@ -201,15 +201,15 @@
"%(roomName)s is not accessible at this time.": "Místnost %(roomName)s není v tuto chvíli dostupná.",
"Save": "Uložit",
"Send Reset Email": "Poslat resetovací e-mail",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s poslal/a obrázek.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s pozval/a uživatele %(targetDisplayName)s ke vstupu do místnosti.",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s poslal(a) obrázek.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s pozval(a) uživatele %(targetDisplayName)s ke vstupu do místnosti.",
"Server error": "Chyba serveru",
"Server may be unavailable, overloaded, or search timed out :(": "Server může být nedostupný, přetížený nebo vyhledávání vypršelo :(",
"Server may be unavailable, overloaded, or you hit a bug.": "Server může být nedostupný, přetížený nebo jste narazili na chybu.",
"Server unavailable, overloaded, or something else went wrong.": "Server je nedostupný, přetížený nebo se něco pokazilo.",
"Session ID": "ID sezení",
"%(senderName)s set a profile picture.": "%(senderName)s si nastavil/a profilový obrázek.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s si změnil/a zobrazované jméno na %(displayName)s.",
"%(senderName)s set a profile picture.": "%(senderName)s si nastavil(a) profilový obrázek.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s si změnil(a) zobrazované jméno na %(displayName)s.",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Zobrazovat čas v 12hodinovém formátu (např. 2:30 odp.)",
"Sign in": "Přihlásit",
"Sign out": "Odhlásit",
@ -235,9 +235,9 @@
"Online": "Online",
"Offline": "Offline",
"Check for update": "Zkontrolovat aktualizace",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s přijal/a pozvání pro %(displayName)s.",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s přijal(a) pozvání pro %(displayName)s.",
"Active call (%(roomName)s)": "Probíhající hovor (%(roomName)s)",
"%(senderName)s banned %(targetName)s.": "%(senderName)s vykázal/a uživatele %(targetName)s.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s vykázal(a) uživatele %(targetName)s.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Nelze se připojit k domovskému serveru přes HTTP, pokud je v adresním řádku HTTPS. Buď použijte HTTPS, nebo <a>povolte nezabezpečené skripty</a>.",
"Click here to fix": "Pro opravu klepněte zde",
"Click to mute video": "Klepněte pro zakázání videa",
@ -258,7 +258,7 @@
"Unable to remove contact information": "Nepodařilo se smazat kontaktní údaje",
"Unable to verify email address.": "Nepodařilo se ověřit e-mailovou adresu.",
"Unban": "Přijmout zpět",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s přijal/a zpět uživatele %(targetName)s.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s přijal(a) zpět uživatele %(targetName)s.",
"Unable to capture screen": "Nepodařilo se zachytit obrazovku",
"Unable to enable Notifications": "Nepodařilo se povolit oznámení",
"unknown caller": "neznámý volající",
@ -305,11 +305,11 @@
"Reason": "Důvod",
"VoIP conference started.": "VoIP konference započata.",
"VoIP conference finished.": "VoIP konference ukončena.",
"%(targetName)s left the room.": "%(targetName)s opustil/a místnost.",
"%(targetName)s left the room.": "%(targetName)s opustil(a) místnost.",
"You are already in a call.": "Již máte probíhající hovor.",
"%(senderName)s requested a VoIP conference.": "Uživatel %(senderName)s požádal o VoIP konferenci.",
"%(senderName)s removed their profile picture.": "%(senderName)s odstranil/a svůj profilový obrázek.",
"%(targetName)s rejected the invitation.": "%(targetName)s odmítl/a pozvání.",
"%(senderName)s removed their profile picture.": "%(senderName)s odstranil(a) svůj profilový obrázek.",
"%(targetName)s rejected the invitation.": "%(targetName)s odmítl(a) pozvání.",
"Communities": "Skupiny",
"Message Pinning": "Připíchnutí zprávy",
"Your browser does not support the required cryptography extensions": "Váš prohlížeč nepodporuje požadovaná kryptografická rozšíření",
@ -320,20 +320,20 @@
"Admin Tools": "Nástroje pro správce",
"No pinned messages.": "Žádné připíchnuté zprávy.",
"Pinned Messages": "Připíchnuté zprávy",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstranil/a své zobrazované jméno (%(oldDisplayName)s).",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s zrušil/a pozvání pro uživatele %(targetName)s.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich pozvání.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich vstupu.",
"%(senderName)s made future room history visible to all room members.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s nastavil/a viditelnost budoucích zpráv pro kohokoliv.",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s změnil/a připíchnuté zprávy této místnosti.",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstranil(a) své zobrazované jméno (%(oldDisplayName)s).",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s zrušil(a) pozvání pro uživatele %(targetName)s.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich pozvání.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich vstupu.",
"%(senderName)s made future room history visible to all room members.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv pro kohokoliv.",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s změnil(a) připíchnuté zprávy této místnosti.",
"Authentication check failed: incorrect password?": "Kontrola ověření selhala: špatné heslo?",
"You need to be able to invite users to do that.": "Pro tuto akci musíte mít právo zvát uživatele.",
"Delete Widget": "Smazat widget",
"Error decrypting image": "Chyba při dešifrování obrázku",
"Error decrypting video": "Chyba při dešifrování videa",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odstranil/a avatar místnosti.",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s změnil/a avatar místnosti na <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odstranil(a) avatar místnosti.",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s změnil(a) avatar místnosti na <img/>",
"Copied!": "Zkopírováno!",
"Failed to copy": "Nepodařilo se zkopírovat",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Smazáním widgetu ho odstraníte všem uživatelům v této místnosti. Opravdu chcete tento widget smazat?",
@ -384,9 +384,9 @@
"Invalid community ID": "Neplatné ID skupiny",
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' není platné ID skupiny",
"New community ID (e.g. +foo:%(localDomain)s)": "Nové ID skupiny (např. +neco:%(localDomain)s)",
"%(senderName)s sent an image": "%(senderName)s poslal/a obrázek",
"%(senderName)s sent a video": "%(senderName)s poslal/a video",
"%(senderName)s uploaded a file": "%(senderName)s nahrál/a soubor",
"%(senderName)s sent an image": "%(senderName)s poslal(a) obrázek",
"%(senderName)s sent a video": "%(senderName)s poslal(a) video",
"%(senderName)s uploaded a file": "%(senderName)s nahrál(a) soubor",
"Disinvite this user?": "Odvolat pozvání tohoto uživatele?",
"Kick this user?": "Vykopnout tohoto uživatele?",
"Unban this user?": "Přijmout zpět tohoto uživatele?",
@ -398,16 +398,16 @@
"You have <a>disabled</a> URL previews by default.": "<a>Vypnuli</a> jste automatické náhledy webových adres.",
"You have <a>enabled</a> URL previews by default.": "<a>Zapnuli</a> jste automatické náhledy webových adres.",
"URL Previews": "Náhledy webových adres",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s změnil/a avatar místnosti %(roomName)s",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s změnil(a) avatar místnosti %(roomName)s",
"Add an Integration": "Přidat začlenění",
"An email has been sent to %(emailAddress)s": "Na adresu %(emailAddress)s jsme poslali e-mail",
"File to import": "Soubor k importu",
"Passphrases must match": "Hesla se musí shodovat",
"Passphrase must not be empty": "Heslo nesmí být prázdné",
"Passphrases must match": "Přístupové fráze se musí shodovat",
"Passphrase must not be empty": "Přístupová fráze nesmí být prázdná",
"Export room keys": "Exportovat klíče místnosti",
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Tento proces vám umožňuje exportovat do souboru klíče ke zprávám, které jste dostali v šifrovaných místnostech. Když pak tento soubor importujete do jiného Matrix klienta, všechny tyto zprávy bude možné opět dešifrovat.",
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kdokoliv, kdo získá přístup k exportovanému souboru, bude moci dešifrovat všechny vaše přijaté zprávy, a proto je třeba dbát zvýšenou pozornost jeho zabezpečení. Z toho důvodu byste měli do kolonky níže zadat heslo, se kterým exportovaná data zašifrujeme. Import pak bude možný pouze se znalostí zadaného hesla.",
"Confirm passphrase": "Potvrďte heslo",
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kdokoliv, kdo získá přístup k exportovanému souboru, bude moci dešifrovat všechny vaše přijaté zprávy, a proto je třeba dbát zvýšenou pozornost jeho zabezpečení. Z toho důvodu byste měli do kolonky níže zadat přístupovou frázi, se kterým exportovaná data zašifrujeme. Import pak bude možný pouze se znalostí zadané přístupové fráze.",
"Confirm passphrase": "Potvrďte přístupovou frázi",
"Import room keys": "Importovat klíče místnosti",
"Call Timeout": "Časový limit hovoru",
"Show these rooms to non-members on the community page and room list?": "Zobrazovat tyto místnosti na domovské stránce skupiny a v seznamu místností i pro nečleny?",
@ -466,18 +466,18 @@
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s%(count)s krát vstoupili",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)svstoupili",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)svstoupil/a",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)svstoupil(a)",
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s %(count)s krát opustili",
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sopustili",
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s %(count)s krát opustil",
"%(oneUser)sleft %(count)s times|one": "%(oneUser)sopustil",
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s %(count)s krát vstoupili a opustili",
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)svstoupili a opustili",
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil/a a opustil/a",
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)svstoupil/a a opustil/a",
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil(a) a opustil(a)",
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)svstoupil(a) a opustil(a)",
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s %(count)s krát opustili a znovu vstoupili",
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sopustili a znovu vstoupili",
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s krát opustil/a a znovu vstoupil/a",
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s krát opustil(a) a znovu vstoupil(a)",
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sopustil a znovu vstoupil",
"%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s %(count)s krát odmítli pozvání",
"%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)sodmítli pozvání",
@ -505,12 +505,12 @@
"was kicked %(count)s times|one": "byl vyhozen",
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s si %(count)s krát změnili jméno",
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s si změnili jméno",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s si %(count)s krát změnil/a jméno",
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s si změnil/ jméno",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s si %(count)s krát změnil(a) jméno",
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s si změnil(a) jméno",
"%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)ssi %(count)s krát změnili avatary",
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)ssi změnili avatary",
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s si %(count)s krát změnil/a avatar",
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s si změnil/a avatar",
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s si %(count)s krát změnil(a) avatar",
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s si změnil(a) avatar",
"%(items)s and %(count)s others|other": "%(items)s a %(count)s další",
"%(items)s and %(count)s others|one": "%(items)s a jeden další",
"%(items)s and %(lastItem)s": "%(items)s a také %(lastItem)s",
@ -539,7 +539,7 @@
"To get started, please pick a username!": "Začněte tím, že si zvolíte uživatelské jméno!",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Toto bude název vašeho účtu na domovském serveru <span></span>, anebo si můžete zvolit <a>jiný server</a>.",
"If you already have a Matrix account you can <a>log in</a> instead.": "Pokud už účet v síti Matrix máte, můžete se ihned <a>Přihlásit</a>.",
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil/a",
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil(a)",
"Private Chat": "Soukromá konverzace",
"Public Chat": "Veřejná konverzace",
"You must <a>register</a> to use this functionality": "Pro využívání této funkce se <a>zaregistrujte</a>",
@ -564,7 +564,7 @@
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Tyto místnosti se zobrazují všem členům na stránce skupiny. Členové skupiny mohou vstoupit do místnosti klepnutím.",
"Featured Rooms:": "Hlavní místnosti:",
"Featured Users:": "Významní uživatelé:",
"%(inviter)s has invited you to join this community": "%(inviter)s vás pozval/a do této skupiny",
"%(inviter)s has invited you to join this community": "%(inviter)s vás pozval(a) do této skupiny",
"You are an administrator of this community": "Jste správcem této skupiny",
"You are a member of this community": "Jste členem této skupiny",
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Vaše skupina nemá vyplněný dlouhý popis, který je součástí HTML stránky skupiny a která se zobrazuje jejím členům.<br />Klepnutím zde otevřete nastavení, kde ho můžete doplnit!",
@ -606,7 +606,7 @@
"Notify the whole room": "Oznámení pro celou místnost",
"Room Notification": "Oznámení místnosti",
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Tento proces vás provede importem šifrovacích klíčů, které jste si stáhli z jiného Matrix klienta. Po úspěšném naimportování budete v tomto klientovi moci dešifrovat všechny zprávy, které jste mohli dešifrovat v původním klientovi.",
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Stažený soubor je chráněn heslem. Soubor můžete naimportovat pouze pokud zadáte odpovídající heslo.",
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Stažený soubor je chráněn přístupovou frází. Soubor můžete naimportovat pouze pokud zadáte odpovídající přístupovou frázi.",
"Call Failed": "Hovor selhal",
"Send": "Odeslat",
"collapse": "sbalit",
@ -753,7 +753,7 @@
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
"Missing roomId.": "Chybějící ID místnosti.",
"Opens the Developer Tools dialog": "Otevře dialog nástrojů pro vývojáře",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s si změnil/a zobrazované jméno na %(displayName)s.",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s si změnil(a) zobrazované jméno na %(displayName)s.",
"Always show encryption icons": "Vždy zobrazovat ikonu stavu šifrovaní",
"Send analytics data": "Odesílat analytická data",
"Enable widget screenshots on supported widgets": "Povolit screenshot widgetu pro podporované widgety",
@ -771,7 +771,7 @@
"System Alerts": "Systémová varování",
"Muted Users": "Umlčení uživatelé",
"Only room administrators will see this warning": "Toto varování uvidí jen správci místnosti",
"You don't currently have any stickerpacks enabled": "Momentálně nemáte aktívní žádné balíčky s nálepkami",
"You don't currently have any stickerpacks enabled": "Momentálně nemáte aktivní žádné balíčky s nálepkami",
"Stickerpack": "Balíček s nálepkami",
"Hide Stickers": "Skrýt nálepky",
"Show Stickers": "Zobrazit nálepky",
@ -943,14 +943,14 @@
"Upgrades a room to a new version": "Upgraduje místnost na novou verzi",
"This room has no topic.": "Tato místnost nemá žádné specifické téma.",
"Sets the room name": "Nastaví název místnosti",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgradoval/a místnost.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s zveřejnil/a místnost pro všechny s odkazem.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s zpřístupnil/a místnost pouze na pozvání.",
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s změnil/a pravidlo k připojení na %(rule)s",
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s povolil/a přístup hostům.",
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s zakázal/a přístup hostům.",
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s změnil/a pravidlo pro přístup hostů na %(rule)s",
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nastavil/a hlavní adresu této místnosti na %(address)s.",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgradoval(a) místnost.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s zveřejnil(a) místnost pro všechny s odkazem.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s zpřístupnil(a) místnost pouze na pozvání.",
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s změnil(a) pravidlo k připojení na %(rule)s",
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s povolil(a) přístup hostům.",
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s zakázal(a) přístup hostům.",
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s změnil(a) pravidlo pro přístup hostů na %(rule)s",
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nastavil(a) hlavní adresu této místnosti na %(address)s.",
"%(senderName)s removed the main address for this room.": "%(senderName)s zrušil hlavní adresu této místnosti.",
"%(displayName)s is typing …": "%(displayName)s píše …",
"%(names)s and %(count)s others are typing …|other": "%(names)s a %(count)s dalších píše …",
@ -1231,10 +1231,10 @@
"You cannot modify widgets in this room.": "V této místnosti nemůžete manipulovat s widgety.",
"Sends the given message coloured as a rainbow": "Pošle zprávu v barvách duhy",
"Sends the given emote coloured as a rainbow": "Pošle reakci v barvách duhy",
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s přidal/a této místnosti příslušnost ke skupině %(groups)s.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s odebral/a této místnosti příslušnost ke skupině %(groups)s.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s přidal/a této místnosti příslušnost ke skupině %(newGroups)s a odebral/a k %(oldGroups)s.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s zrušil/a pozvání do této místnosti pro uživatele %(targetDisplayName)s.",
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s přidal(a) této místnosti příslušnost ke skupině %(groups)s.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s odebral(a) této místnosti příslušnost ke skupině %(groups)s.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s přidal(a) této místnosti příslušnost ke skupině %(newGroups)s a odebral(a) k %(oldGroups)s.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s zrušil(a) pozvání do této místnosti pro uživatele %(targetDisplayName)s.",
"No homeserver URL provided": "Nebyla zadána URL adresa domovského server",
"Unexpected error resolving homeserver configuration": "Chyba při zjišťování konfigurace domovského serveru",
"The user's homeserver does not support the version of the room.": "Uživatelův domovský server nepodporuje verzi této místnosti.",
@ -1253,11 +1253,11 @@
"Join the conversation with an account": "Připojte se ke konverzaci s účtem",
"Sign Up": "Zaregistrovat se",
"Sign In": "Přihlásit se",
"You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s vás vykopl/a z místnosti %(roomName)s",
"You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s vás vykopl(a) z místnosti %(roomName)s",
"Reason: %(reason)s": "Důvod: %(reason)s",
"Forget this room": "Zapomenout na tuto místnost",
"Re-join": "Znovu vstoupit",
"You were banned from %(roomName)s by %(memberName)s": "%(memberName)s vás vykázal/a z místnosti %(roomName)s",
"You were banned from %(roomName)s by %(memberName)s": "%(memberName)s vás vykázal(a) z místnosti %(roomName)s",
"Something went wrong with your invite to %(roomName)s": "S vaší pozvánkou do místnosti %(roomName)s se něco pokazilo",
"You can only join it with a working invite.": "Vstoupit můžete jen s funkční pozvánkou.",
"You can still join it because this is a public room.": "I přesto můžete vstoupit, protože tato místnost je veřejná.",
@ -1265,7 +1265,7 @@
"Try to join anyway": "Stejně se pokusit vstoupit",
"Do you want to chat with %(user)s?": "Chcete si povídat s %(user)s?",
"Do you want to join %(roomName)s?": "Chcete vstoupit do místnosti %(roomName)s?",
"<userName/> invited you": "<userName/> vás pozval/a",
"<userName/> invited you": "<userName/> vás pozval(a)",
"You're previewing %(roomName)s. Want to join it?": "Nahlížíte do místnosti %(roomName)s. Chcete do ní vstoupit?",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s si nelze jen tak prohlížet. Chcete do ní vstoupit?",
"This room doesn't exist. Are you sure you're at the right place?": "Tato místnost neexistuje. Jste si jistí, že jste na správném místě?",
@ -1280,7 +1280,7 @@
"Invited by %(sender)s": "Pozván od uživatele %(sender)s",
"Error updating flair": "Nepovedlo se změnit příslušnost ke skupině",
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Pro tuto místnost se nepovedlo změnit příslušnost ke skupině. Možná to server neumožňuje, nebo došlo k dočasné chybě.",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reagoval/a s %(shortName)s</reactedWith>",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reagoval(a) s %(shortName)s</reactedWith>",
"edited": "upraveno",
"Maximize apps": "Maximalizovat aplikace",
"Rotate Left": "Otočit doleva",
@ -1509,11 +1509,11 @@
"Show image": "Zobrazit obrázek",
"You verified %(name)s": "Ověřili jste %(name)s",
"You cancelled verifying %(name)s": "Zrušili jste ověření %(name)s",
"%(name)s cancelled verifying": "%(name)s zrušil/a ověření",
"%(name)s cancelled verifying": "%(name)s zrušil(a) ověření",
"You accepted": "Přijali jste",
"%(name)s accepted": "%(name)s přijal/a",
"%(name)s accepted": "%(name)s přijal(a)",
"You cancelled": "Zrušili jste",
"%(name)s cancelled": "%(name)s zrušil/a",
"%(name)s cancelled": "%(name)s zrušil(a)",
"%(name)s wants to verify": "%(name)s chce ověřit",
"You sent a verification request": "Poslali jste požadavek na ověření",
"Show all": "Zobrazit vše",
@ -1532,8 +1532,8 @@
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Vyrobte prosím <newIssueLink>nové issue</newIssueLink> na GitHubu abychom mohli chybu opravit.",
"%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s neudělali %(count)s krát žádnou změnu",
"%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s neudělali žádnou změnu",
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s neudělal/a %(count)s krát žádnou změnu",
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s neudělal/a žádnou změnu",
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s neudělal(a) %(count)s krát žádnou změnu",
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s neudělal(a) žádnou změnu",
"e.g. my-room": "např. moje-mistnost",
"Use bots, bridges, widgets and sticker packs": "Použít roboty, propojení, widgety a balíky samolepek",
"Terms of Service": "Podmínky použití",
@ -1555,7 +1555,7 @@
"Explore": "Procházet",
"Filter": "Filtr místností",
"Filter rooms…": "Najít místnost…",
"%(creator)s created and configured the room.": "%(creator)s vytvořil/a a nakonfiguroval/a místnost.",
"%(creator)s created and configured the room.": "%(creator)s vytvořil(a) a nakonfiguroval(a) místnost.",
"Preview": "Náhled",
"View": "Zobrazit",
"Find a room…": "Najít místnost…",
@ -1589,27 +1589,27 @@
"Custom (%(level)s)": "Vlastní (%(level)s)",
"Error upgrading room": "Chyba při upgrade místnosti",
"Double check that your server supports the room version chosen and try again.": "Zkontrolujte, že váš server opravdu podporuje zvolenou verzi místnosti.",
"%(senderName)s placed a voice call.": "%(senderName)s spustil hovor.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s spustil hovor. (není podporováno tímto prohlížečem)",
"%(senderName)s placed a video call.": "%(senderName)s spustil videohovor.",
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s spustil videohovor. (není podporováno tímto prohlížečem)",
"%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s odstranil/a pravidlo blokující uživatele odpovídající %(glob)s",
"%(senderName)s placed a voice call.": "%(senderName)s zahájil(a) hovor.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s zahájil(a) hovor. (není podporováno tímto prohlížečem)",
"%(senderName)s placed a video call.": "%(senderName)s zahájil(a) videohovor.",
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s zahájil(a) videohovor. (není podporováno tímto prohlížečem)",
"%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s odstranil(a) pravidlo blokující uživatele odpovídající %(glob)s",
"%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s odstranil pravidlo blokující místnosti odpovídající %(glob)s",
"%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s odstranil pravidlo blokující servery odpovídající %(glob)s",
"%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s odstranil blokující pravidlo %(glob)s",
"%(senderName)s updated an invalid ban rule": "%(senderName)s aktualizoval neplatné pravidlo blokování",
"%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval/a pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval(a) pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo blokující místnosti odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo blokující servery odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval blokovací pravidlo odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s vytvořil/a pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s vytvořil(a) pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s vytvořil pravidlo blokující místnosti odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s vytvořil pravidlo blokující servery odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s vytvořil blokovací pravidlo odpovídající %(glob)s z důvodu %(reason)s",
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující uživatele odpovídající %(oldGlob)s na uživatele odpovídající %(newGlob)s z důvodu %(reason)s",
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující místnosti odpovídající %(oldGlob)s na místnosti odpovídající %(newGlob)s z důvodu %(reason)s",
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující servery odpovídající %(oldGlob)s na servery odpovídající %(newGlob)s z důvodu %(reason)s",
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a blokovací pravidlo odpovídající %(oldGlob)s na odpovídající %(newGlob)s z důvodu %(reason)s",
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) pravidlo blokující uživatele odpovídající %(oldGlob)s na uživatele odpovídající %(newGlob)s z důvodu %(reason)s",
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) pravidlo blokující místnosti odpovídající %(oldGlob)s na místnosti odpovídající %(newGlob)s z důvodu %(reason)s",
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) pravidlo blokující servery odpovídající %(oldGlob)s na servery odpovídající %(newGlob)s z důvodu %(reason)s",
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) blokovací pravidlo odpovídající %(oldGlob)s na odpovídající %(newGlob)s z důvodu %(reason)s",
"Try out new ways to ignore people (experimental)": "Vyzkoušejte nové metody ignorování lidí (experimentální)",
"Match system theme": "Nastavit podle vzhledu systému",
"My Ban List": "Můj seznam zablokovaných",
@ -1873,7 +1873,7 @@
"New session": "Nová relace",
"Use this session to verify your new one, granting it access to encrypted messages:": "Použijte tuto relaci pro ověření nové a udělení jí přístupu k vašim šifrovaným zprávám:",
"If you didnt sign in to this session, your account may be compromised.": "Pokud jste se do této nové relace nepřihlásili, váš účet může být kompromitován.",
"This wasn't me": "To jsem nebyl/a já",
"This wasn't me": "To jsem nebyl(a) já",
"This will allow you to return to your account after signing out, and sign in on other sessions.": "Toto vám umožní se přihlásit do dalších relací a vrátit se ke svému účtu poté, co se odhlásíte.",
"Recovery key mismatch": "Obnovovací klíč neodpovídá",
"Incorrect recovery passphrase": "Nesprávné heslo pro obnovení",
@ -1893,7 +1893,7 @@
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Varování: Vaše osobní data (včetně šifrovacích klíčů) jsou tu pořád uložena. Smažte je, pokud chcete tuto relaci zahodit, nebo se přihlaste pod jiný účet.",
"If you cancel now, you won't complete verifying the other user.": "Pokud teď proces zrušíte, tak nebude druhý uživatel ověřen.",
"If you cancel now, you won't complete verifying your other session.": "Pokud teď proces zrušíte, tak nebude druhá relace ověřena.",
"Cancel entering passphrase?": "Zrušit zadávání hesla?",
"Cancel entering passphrase?": "Zrušit zadávání přístupové fráze?",
"Setting up keys": "Příprava klíčů",
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)su chybí nějaké komponenty, které jsou potřeba pro vyhledávání v zabezpečených místnostech. Pokud chcete s touto funkcí experimentovat, tak si pořiďte vlastní %(brand)s Desktop s <nativeLink>přidanými komponentami</nativeLink>.",
"Subscribing to a ban list will cause you to join it!": "Odebíráním seznamu zablokovaných uživatelů se přidáte do jeho místnosti!",
@ -1922,7 +1922,7 @@
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "Relace, kterou se snažíte ověřit, neumožňuje ověření QR kódem ani pomocí emoji, což je to, co %(brand)s podporuje. Zkuste použít jiného klienta.",
"Verify by scanning": "Ověřte naskenováním",
"You declined": "Odmítli jste",
"%(name)s declined": "%(name)s odmítl/a",
"%(name)s declined": "%(name)s odmítl(a)",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Uschovejte si kopii na bezpečném místě, například ve správci hesel nebo v trezoru.",
"Your recovery key": "Váš obnovovací klíč",
"Copy": "Zkopírovat",
@ -1948,7 +1948,7 @@
"Show shortcuts to recently viewed rooms above the room list": "Zobrazovat zkratky do nedávno zobrazených místností navrchu",
"Cancelling…": "Rušení…",
"Your homeserver does not support cross-signing.": "Váš domovský server nepodporuje cross-signing.",
"Homeserver feature support:": "Funkce podporované domovským serverem:",
"Homeserver feature support:": "Funkce podporovaná domovským serverem:",
"Accepting…": "Přijímání…",
"Accepting …": "Přijímání…",
"Declining …": "Odmítání…",
@ -1963,14 +1963,14 @@
"Mark all as read": "Označit vše jako přečtené",
"Not currently indexing messages for any room.": "Aktuálně neindexujeme žádné zprávy.",
"%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s z %(totalRooms)s",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s změnil/a jméno místnosti z %(oldRoomName)s na %(newRoomName)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s přidal/a této místnosti alternativní adresy %(addresses)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s přidal/a této místnosti alternativní adresu %(addresses)s.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odebral/a této místnosti alternativní adresy %(addresses)s.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odebral/a této místnosti alternativní adresu %(addresses)s.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s změnil/a alternativní adresy této místnosti.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s změnil/a hlavní a alternativní adresy této místnosti.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s změnil/a adresy této místnosti.",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s změnil(a) jméno místnosti z %(oldRoomName)s na %(newRoomName)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s přidal(a) této místnosti alternativní adresy %(addresses)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s přidal(a) této místnosti alternativní adresu %(addresses)s.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odebral(a) této místnosti alternativní adresy %(addresses)s.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odebral(a) této místnosti alternativní adresu %(addresses)s.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s změnil(a) alternativní adresy této místnosti.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s změnil(a) hlavní a alternativní adresy této místnosti.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s změnil(a) adresy této místnosti.",
"Manually Verify by Text": "Manuální textové ověření",
"Interactively verify by Emoji": "Interaktivní ověření s emotikonami",
"Support adding custom themes": "Umožnit přidání vlastního vzhledu",
@ -2044,7 +2044,7 @@
"Start verification again from their profile.": "Proces ověření začněte znovu z profilu kontaktu.",
"Verification timed out.": "Ověření vypršelo.",
"You cancelled verification on your other session.": "Na druhé relace jste proces ověření zrušili.",
"%(displayName)s cancelled verification.": "%(displayName)s zrušil/a proces ověření.",
"%(displayName)s cancelled verification.": "%(displayName)s zrušil(a) proces ověření.",
"You cancelled verification.": "Zrušili jste proces ověření.",
"Message deleted": "Zpráva smazána",
"Message deleted by %(name)s": "Zpráva smazána uživatelem %(name)s",
@ -2104,7 +2104,7 @@
"Restart": "Restartovat",
"Upgrade your %(brand)s": "Aktualizovat %(brand)s",
"A new version of %(brand)s is available!": "Je dostupná nová verze %(brand)su!",
"Are you sure you want to cancel entering passphrase?": "Chcete určitě zrušit zadávání hesla?",
"Are you sure you want to cancel entering passphrase?": "Chcete určitě zrušit zadávání přístupové fráze?",
"Use your account to sign in to the latest version": "Přihlašte se za pomoci svého účtu do nejnovější verze",
"Riot is now Element!": "Riot je nyní Element!",
"Learn More": "Zjistit více",
@ -2172,7 +2172,7 @@
"%(senderName)s left the call": "%(senderName)s opustil/a hovor",
"Call ended": "Hovor skončil",
"You started a call": "Začali jste hovor",
"%(senderName)s started a call": "%(senderName)s začal/a hovor",
"%(senderName)s started a call": "%(senderName)s začal(a) hovor",
"Waiting for answer": "Čekání na odpověď",
"%(senderName)s is calling": "%(senderName)s volá",
"* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
@ -2193,7 +2193,7 @@
"Incoming call": "Příchozí hovor",
"Your server isn't responding to some <a>requests</a>.": "Váš server neodpovídá na některé <a>požadavky</a>.",
"Master private key:": "Hlavní soukromý klíč:",
"%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s nemůže v prohlížeči lokálně bezpečně uložit zprávy. Použijte <desktopLink>%(brand)s Desktop</desktopLink> aby fungovalo vyhledávání v šifrovaných zprávách.",
"%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s nemůže bezpečně ukládat šifrované zprávy lokálně v prohlížeči. Pro zobrazení šifrovaných zpráv ve výsledcích vyhledávání použijte <desktopLink>%(brand)s Desktop</desktopLink>.",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Váš administrátor vypnul šifrování ve výchozím nastavení soukromých místností a přímých chatů.",
"To link to this room, please add an address.": "Přidejte prosím místnosti adresu aby na ní šlo odkazovat.",
"The authenticity of this encrypted message can't be guaranteed on this device.": "Pravost této šifrované zprávy nelze na tomto zařízení ověřit.",
@ -2256,7 +2256,7 @@
"Start a new chat": "Založit novou konverzaci",
"Which officially provided instance you are using, if any": "Kterou oficiální instanci Riot.im používáte (a jestli vůbec)",
"Change the topic of this room": "Změnit téma této místnosti",
"%(senderName)s declined the call.": "%(senderName)s odmítl/a hovor.",
"%(senderName)s declined the call.": "%(senderName)s odmítl(a) hovor.",
"(an error occurred)": "(došlo k chybě)",
"(their device couldn't start the camera / microphone)": "(zařízení druhé strany nemohlo spustit kameru / mikrofon)",
"(connection failed)": "(spojení selhalo)",
@ -2295,7 +2295,7 @@
"Attach files from chat or just drag and drop them anywhere in a room.": "Připojte soubory z chatu nebo je jednoduše přetáhněte kamkoli do místnosti.",
"No files visible in this room": "V této místnosti nejsou viditelné žádné soubory",
"Show files": "Zobrazit soubory",
"%(count)s people|other": "%(count)s lidé/í",
"%(count)s people|other": "%(count)s osob",
"About": "O",
"Youre all caught up": "Vše vyřízeno",
"You have no visible notifications in this room.": "V této místnosti nemáte žádná viditelná oznámení.",
@ -2333,10 +2333,10 @@
"Switch to light mode": "Přepnout do světlého režimu",
"User settings": "Uživatelská nastavení",
"Community settings": "Nastavení skupiny",
"Confirm your recovery passphrase": "Potvrďte vaši frázi pro obnovení",
"Confirm your recovery passphrase": "Potvrďte vaši přístupovou frázi pro obnovení",
"Repeat your recovery passphrase...": "Opakujte přístupovou frázi pro obnovení...",
"Please enter your recovery passphrase a second time to confirm.": "Potvrďte prosím podruhé svou frázi pro obnovení.",
"Use a different passphrase?": "Použít jinou frázi?",
"Use a different passphrase?": "Použít jinou přístupovou frázi?",
"Great! This recovery passphrase looks strong enough.": "Skvělé! Tato fráze pro obnovení vypadá dostatečně silně.",
"Enter a recovery passphrase": "Zadejte frázi pro obnovení",
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s nebo %(usernamePassword)s",
@ -2356,7 +2356,7 @@
"delete the address.": "smazat adresu.",
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Smazat adresu místnosti %(alias)s a odebrat %(name)s z adresáře?",
"Self-verification request": "Požadavek na sebeověření",
"%(creator)s created this DM.": "%(creator)s vytvořil tuto přímou zprávu.",
"%(creator)s created this DM.": "%(creator)s vytvořil(a) tuto přímou zprávu.",
"You do not have permission to create rooms in this community.": "Nemáte oprávnění k vytváření místností v této skupině.",
"Cannot create rooms in this community": "V této skupině nelze vytvořit místnosti",
"Great, that'll help people know it's you": "Skvělé, to pomůže lidem zjistit, že jste to vy",
@ -2439,7 +2439,7 @@
"There are two ways you can provide feedback and help us improve %(brand)s.": "Jsou dva způsoby, jak můžete poskytnout zpětnou vazbu a pomoci nám vylepšit %(brand)s.",
"Rate %(brand)s": "Ohodnotit %(brand)s",
"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.": "V této relaci jste již dříve používali novější verzi %(brand)s. Chcete-li tuto verzi znovu použít s šifrováním, budete se muset odhlásit a znovu přihlásit.",
"Homeserver": "Homeserver",
"Homeserver": "Domovský server",
"Continue with %(provider)s": "Pokračovat s %(provider)s",
"Server Options": "Možnosti serveru",
"This version of %(brand)s does not support viewing some encrypted files": "Tato verze %(brand)s nepodporuje zobrazení některých šifrovaných souborů",
@ -2644,7 +2644,7 @@
"Enter name": "Zadejte jméno",
"Ignored attempt to disable encryption": "Ignorovaný pokus o deaktivaci šifrování",
"Show chat effects": "Zobrazit efekty chatu",
"%(count)s people|one": "%(count)s člověk",
"%(count)s people|one": "%(count)s osoba",
"Takes the call in the current room off hold": "Zruší podržení hovoru v aktuální místnosti",
"Places the call in the current room on hold": "Podrží hovor v aktuální místnosti",
"Zimbabwe": "Zimbabwe",
@ -2789,7 +2789,7 @@
"Fill Screen": "Vyplnit obrazovku",
"Voice Call": "Hlasový hovor",
"Video Call": "Videohovor",
"%(senderName)s ended the call": "%(senderName)s ukončil/a hovor",
"%(senderName)s ended the call": "%(senderName)s ukončil(a) hovor",
"You ended the call": "Ukončili jste hovor",
"New version of %(brand)s is available": "K dispozici je nová verze %(brand)s",
"Error leaving room": "Při opouštění místnosti došlo k chybě",
@ -2879,7 +2879,7 @@
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Vloží (╯°□°)╯︵ ┻━┻ na začátek zprávy",
"Remain on your screen while running": "Při běhu zůstává na obrazovce",
"Remain on your screen when viewing another room, when running": "Při prohlížení jiné místnosti zůstává při běhu na obrazovce",
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s změnil/a seznam přístupů serveru pro tuto místnost.",
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s změnil(a) seznam přístupů serveru pro tuto místnost.",
"Offline encrypted messaging using dehydrated devices": "Offline šifrovaná komunikace pomocí dehydrovaných zařízení",
"See emotes posted to your active room": "Prohlédněte si emoji zveřejněné ve vaší aktivní místnosti",
"See emotes posted to this room": "Prohlédněte si emoji zveřejněné v této místnosti",
@ -2968,5 +2968,18 @@
"Share your screen": "Sdílejte svou obrazovku",
"Expand code blocks by default": "Ve výchozím nastavení rozbalit bloky kódu",
"Show line numbers in code blocks": "Zobrazit čísla řádků v blocích kódu",
"Recently visited rooms": "Nedávno navštívené místnosti"
"Recently visited rooms": "Nedávno navštívené místnosti",
"Upgrade to pro": "Upgradujte na profesionální verzi",
"Minimize dialog": "Minimalizovat dialog",
"Maximize dialog": "Maximalizovat dialog",
"%(hostSignupBrand)s Setup": "Nastavení %(hostSignupBrand)s",
"You should know": "Měli byste vědět",
"Privacy Policy": "Zásady ochrany osobních údajů",
"Cookie Policy": "Zásady používání souborů cookie",
"Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Další informace najdete v našich <privacyPolicyLink />, <termsOfServiceLink /> a <cookiePolicyLink />.",
"Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Dočasné pokračování umožňuje procesu nastavení %(hostSignupBrand)s přístup k vašemu účtu za účelem načtení ověřených e-mailových adres. Tato data se neukládají.",
"Failed to connect to your homeserver. Please close this dialog and try again.": "Připojení k domovskému serveru se nezdařilo. Zavřete toto dialogové okno a zkuste to znovu.",
"Abort": "Přerušit",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Opravdu chcete přerušit vytváření hostitele? Proces nemůže být navázán.",
"Confirm abort of host creation": "Potvrďte přerušení vytváření hostitele"
}

View file

@ -12,10 +12,10 @@
"Name": "Name",
"Session ID": "Sitzungs-ID",
"Displays action": "Zeigt Aktionen an",
"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",
"Bans user with given id": "Verbannt den/die Benutzer:in mit der angegebenen ID",
"Deops user with given id": "Setzt das Berechtigungslevel des/der Benutzer:in mit der angegebenen ID zurück",
"Invites user with given id to current room": "Lädt den/die Benutzer:in mit der angegebenen ID in den aktuellen Raum ein",
"Kicks user with given id": "Benutzer:in mit der angegebenen ID kicken",
"Changes your display nickname": "Ändert deinen angezeigten Nicknamen",
"Change Password": "Passwort ändern",
"Searches DuckDuckGo for results": "Verwendet DuckDuckGo für Suchergebnisse",
@ -35,7 +35,7 @@
"Deactivate Account": "Benutzerkonto deaktivieren",
"Failed to send email": "Fehler beim Senden der E-Mail",
"Account": "Benutzerkonto",
"Click here to fix": "Zum reparieren hier klicken",
"Click here to fix": "Zum Reparieren hier klicken",
"Default": "Standard",
"Export E2E room keys": "E2E-Raum-Schlüssel exportieren",
"Failed to change password. Is your password correct?": "Passwortänderung fehlgeschlagen. Ist dein Passwort richtig?",
@ -83,13 +83,13 @@
"Server may be unavailable, overloaded, or you hit a bug.": "Server ist nicht verfügbar, überlastet oder du bist auf einen Softwarefehler gestoßen.",
"Labs": "Labor",
"Unable to add email address": "E-Mail-Adresse konnte nicht hinzugefügt werden",
"Unable to remove contact information": "Die Kontakt-Informationen konnten nicht gelöscht werden",
"Unable to remove contact information": "Die Kontaktinformationen können nicht gelöscht werden",
"Unable to verify email address.": "Die E-Mail-Adresse konnte nicht verifiziert werden.",
"Unban": "Verbannung aufheben",
"unknown error code": "Unbekannter Fehlercode",
"Upload avatar": "Profilbild hochladen",
"Upload file": "Datei hochladen",
"Users": "Benutzer",
"Users": "Nutzer:innen",
"Verification Pending": "Verifizierung ausstehend",
"Video call": "Videoanruf",
"Voice call": "Sprachanruf",
@ -131,7 +131,7 @@
"Jun": "Jun",
"Jul": "Juli",
"Aug": "Aug",
"Sep": "Sep",
"Sep": "Sept",
"Oct": "Okt",
"Nov": "Nov",
"Dec": "Dez",
@ -160,7 +160,7 @@
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s hat den zukünftigen Chatverlauf für alle Raum-Mitglieder sichtbar gemacht (ab dem Zeitpunkt, an dem sie eingeladen wurden).",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s hat den zukünftigen Chatverlauf für alle Raum-Mitglieder sichtbar gemacht (ab dem Zeitpunkt, an dem sie beigetreten sind).",
"%(senderName)s made future room history visible to all room members.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für: Alle Raum-Mitglieder.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für: Alle.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s hat den zukünftigen Chatverlauf für alle sichtbar gemacht.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s hat den zukünftigen Chatverlauf für Unbekannte sichtbar gemacht (%(visibility)s).",
"Missing room_id in request": "Fehlende room_id in Anfrage",
"Missing user_id in request": "Fehlende user_id in Anfrage",
@ -181,7 +181,7 @@
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s hat die Verbannung von %(targetName)s aufgehoben.",
"Usage": "Verwendung",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen.",
"You need to be able to invite users to do that.": "Du musst die Berechtigung haben, Benutzer einzuladen, um diese Aktion ausführen zu können.",
"You need to be able to invite users to do that.": "Du brauchst die Berechtigung Benutzer:innen einzuladen haben, um diese Aktion ausführen zu können.",
"You need to be logged in.": "Du musst angemeldet sein.",
"There are no visible files in this room": "Es gibt keine sichtbaren Dateien in diesem Raum",
"Connectivity to the server has been lost.": "Verbindung zum Server wurde unterbrochen.",
@ -285,7 +285,7 @@
"Error decrypting video": "Video-Entschlüsselung fehlgeschlagen",
"Import room keys": "Raum-Schlüssel importieren",
"File to import": "Zu importierende Datei",
"Failed to invite the following users to the %(roomName)s room:": "Folgende Benutzer konnten nicht in den Raum \"%(roomName)s\" eingeladen werden:",
"Failed to invite the following users to the %(roomName)s room:": "Folgende Benutzer:innen konnten nicht in den Raum \"%(roomName)s\" eingeladen werden:",
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Bist du sicher, dass du dieses Ereignis entfernen (löschen) möchtest? Wenn du die Änderung eines Raum-Namens oder eines Raum-Themas löscht, kann dies dazu führen, dass die ursprüngliche Änderung rückgängig gemacht wird.",
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Dieser Prozess erlaubt es dir, die Schlüssel für die in verschlüsselten Räumen empfangenen Nachrichten in eine lokale Datei zu exportieren. In Zukunft wird es möglich sein, diese Datei in einen anderen Matrix-Client zu importieren, sodass dieser Client diese Nachrichten ebenfalls entschlüsseln kann.",
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Mit der exportierten Datei kann jeder, der diese Datei lesen kann, jede verschlüsselte Nachricht entschlüsseln, die für dich lesbar ist. Du solltest die Datei also unbedingt sicher verwahren. Um den Vorgang sicherer zu gestalten, solltest du unten eine Passphrase eingeben, die dazu verwendet wird, die exportierten Daten zu verschlüsseln. Anschließend wird es nur möglich sein, die Daten zu importieren, wenn dieselbe Passphrase verwendet wird.",
@ -311,7 +311,7 @@
"No Microphones detected": "Keine Mikrofone erkannt",
"No media permissions": "Keine Medienberechtigungen",
"You may need to manually permit %(brand)s to access your microphone/webcam": "Gegebenenfalls kann es notwendig sein, dass du %(brand)s manuell den Zugriff auf dein Mikrofon bzw. deine Webcam gewähren musst",
"Default Device": "Standard-Gerät",
"Default Device": "Standardgerät",
"Microphone": "Mikrofon",
"Camera": "Kamera",
"Export": "Exportieren",
@ -357,7 +357,7 @@
"Custom": "Erweitert",
"Decline": "Ablehnen",
"Drop File Here": "Lasse Datei hier los",
"Failed to upload profile picture!": "Hochladen des Profilbild's fehlgeschlagen!",
"Failed to upload profile picture!": "Hochladen des Profilbilds fehlgeschlagen!",
"Incoming call from %(name)s": "Eingehender Anruf von %(name)s",
"Incoming video call from %(name)s": "Eingehender Videoanruf von %(name)s",
"Incoming voice call from %(name)s": "Eingehender Sprachanruf von %(name)s",
@ -416,31 +416,31 @@
"You are now ignoring %(userId)s": "%(userId)s wird jetzt ignoriert",
"You are no longer ignoring %(userId)s": "%(userId)s wird nicht mehr ignoriert",
"Leave": "Verlassen",
"Failed to invite the following users to %(groupId)s:": "Die folgenden Benutzer konnten nicht in die Gruppe %(groupId)s eingeladen werden:",
"Failed to invite the following users to %(groupId)s:": "Die folgenden Benutzer:innen konnten nicht in die Gruppe %(groupId)s eingeladen werden:",
"Leave %(groupName)s?": "%(groupName)s verlassen?",
"Add a Room": "Raum hinzufügen",
"Add a User": "Benutzer hinzufügen",
"Add a User": "Benutzer:in hinzufügen",
"You have entered an invalid address.": "Du hast eine ungültige Adresse eingegeben.",
"Matrix ID": "Matrix-ID",
"Unignore": "Nicht mehr ignorieren",
"Unignored user": "Benutzer nicht mehr ignoriert",
"Ignored user": "Benutzer ignoriert",
"Unignored user": "Benutzer:in nicht mehr ignoriert",
"Ignored user": "Benutzer:in ignoriert",
"Stops ignoring a user, showing their messages going forward": "Beendet das Ignorieren eines Benutzers, nachfolgende Nachrichten werden wieder angezeigt",
"Ignores a user, hiding their messages from you": "Ignoriert einen Benutzer und verbirgt dessen Nachrichten",
"Ignores a user, hiding their messages from you": "Ignoriert eine:n Benutzer:in und verbirgt dessen/deren Nachrichten",
"Banned by %(displayName)s": "Verbannt von %(displayName)s",
"Description": "Beschreibung",
"Unable to accept invite": "Einladung kann nicht angenommen werden",
"Failed to invite users to %(groupId)s": "Benutzer konnten nicht in %(groupId)s eingeladen werden",
"Failed to invite users to %(groupId)s": "Benutzer:innen konnten nicht in %(groupId)s eingeladen werden",
"Unable to reject invite": "Einladung konnte nicht abgelehnt werden",
"Who would you like to add to this summary?": "Wen möchtest zu dieser Übersicht hinzufügen?",
"Add to summary": "Zur Übersicht hinzufügen",
"Failed to add the following users to the summary of %(groupId)s:": "Die folgenden Benutzer konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
"Failed to add the following users to the summary of %(groupId)s:": "Die folgenden Benutzer:innen konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
"Which rooms would you like to add to this summary?": "Welche Räume möchtest du zu dieser Übersicht hinzufügen?",
"Failed to add the following rooms to the summary of %(groupId)s:": "Die folgenden Räume konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
"Failed to remove the room from the summary of %(groupId)s": "Der Raum konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
"The room '%(roomName)s' could not be removed from the summary.": "Der Raum '%(roomName)s' konnte nicht aus der Übersicht entfernt werden.",
"Failed to remove a user from the summary of %(groupId)s": "Benutzer konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
"The user '%(displayName)s' could not be removed from the summary.": "Der Benutzer '%(displayName)s' konnte nicht aus der Übersicht entfernt werden.",
"Failed to remove a user from the summary of %(groupId)s": "Benutzer:in konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
"The user '%(displayName)s' could not be removed from the summary.": "Der Benutzer:in '%(displayName)s' konnte nicht aus der Übersicht entfernt werden.",
"Unknown": "Unbekannt",
"Failed to add the following rooms to %(groupId)s:": "Die folgenden Räume konnten nicht zu %(groupId)s hinzugefügt werden:",
"Matrix Room ID": "Matrix-Raum-ID",
@ -469,7 +469,7 @@
"Which rooms would you like to add to this community?": "Welche Räume sollen zu dieser Community hinzugefügt werden?",
"Add rooms to the community": "Räume zur Community hinzufügen",
"Add to community": "Zur Community hinzufügen",
"Failed to invite users to community": "Benutzer konnten nicht in die Community eingeladen werden",
"Failed to invite users to community": "Benutzer:innen konnten nicht in die Community eingeladen werden",
"Communities": "Communities",
"Invalid community ID": "Ungültige Community-ID",
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' ist keine gültige Community-ID",
@ -485,20 +485,20 @@
"Community ID": "Community-ID",
"example": "Beispiel",
"Add rooms to the community summary": "Fügt Räume zur Community-Übersicht hinzu",
"Add users to the community summary": "Füge Benutzer zur Community-Übersicht hinzu",
"Add users to the community summary": "Füge Benutzer:innen zur Community-Übersicht hinzu",
"Failed to update community": "Aktualisieren der Community fehlgeschlagen",
"Leave Community": "Community verlassen",
"Add rooms to this community": "Räume zu dieser Community hinzufügen",
"%(inviter)s has invited you to join this community": "%(inviter)s hat dich in diese Community eingeladen",
"You are a member of this community": "Du bist Mitglied dieser Community",
"You are an administrator of this community": "Du bist ein Administrator dieser Community",
"You are an administrator of this community": "Du bist ein:e Administrator:in dieser Community",
"Community %(groupId)s not found": "Community '%(groupId)s' nicht gefunden",
"Failed to load %(groupId)s": "'%(groupId)s' konnte nicht geladen werden",
"Error whilst fetching joined communities": "Fehler beim Laden beigetretener Communities",
"Create a new community": "Neue Community erstellen",
"Your Communities": "Deine Communities",
"You're not currently a member of any communities.": "Du gehörst aktuell keiner Community an.",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Erstelle eine Community, um Benutzer und Räume miteinander zu verbinden! Erstelle zusätzlich eine eigene Homepage, um deinen individuellen Bereich im Matrix-Universum zu gestalten.",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Erstelle eine Community, um Benutzer:innen und Räume miteinander zu verbinden! Erstelle zusätzlich eine eigene Homepage, um deinen individuellen Bereich im Matrix-Universum zu gestalten.",
"Something went wrong whilst creating your community": "Beim Erstellen deiner Community ist ein Fehler aufgetreten",
"And %(count)s more...|other": "Und %(count)s weitere...",
"Delete Widget": "Widget löschen",
@ -550,17 +550,17 @@
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)shaben das Profilbild geändert",
"%(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 kicken?",
"Unban this user?": "Verbannung für diesen Benutzer aufheben?",
"Ban this user?": "Diesen Benutzer verbannen?",
"Disinvite this user?": "Einladung für diese:n Benutzer:in zurückziehen?",
"Kick this user?": "Diese:n Benutzer:in kicken?",
"Unban this user?": "Verbannung für diese:n Benutzer:in aufheben?",
"Ban this user?": "Diese:n Benutzer:in verbannen?",
"Members only (since the point in time of selecting this option)": "Nur Mitglieder (ab dem Zeitpunkt, an dem diese Option ausgewählt wird)",
"Members only (since they were invited)": "Nur Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden)",
"Members only (since they joined)": "Nur Mitglieder (ab dem Zeitpunkt, an dem sie beigetreten sind)",
"An email has been sent to %(emailAddress)s": "Eine E-Mail wurde an %(emailAddress)s gesendet",
"A text message has been sent to %(msisdn)s": "Eine Textnachricht wurde an %(msisdn)s gesendet",
"Disinvite this user from community?": "Community-Einladung für diesen Benutzer zurückziehen?",
"Remove this user from community?": "Diesen Benutzer aus der Community entfernen?",
"Disinvite this user from community?": "Community-Einladung für diese:n Benutzer:in zurückziehen?",
"Remove this user from community?": "Diese:n Benutzer:in aus der Community entfernen?",
"%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)shaben ihre Einladungen %(count)s-mal abgelehnt",
"%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)shat die Einladung %(count)s-mal abgelehnt",
"%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)shat die Einladung abgelehnt",
@ -629,7 +629,7 @@
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Ob du den Richtext-Modus des Editors benutzt oder nicht",
"Your homeserver's URL": "Deine Homeserver-URL",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der/die letze Nutzer:in mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.",
"Community IDs cannot be empty.": "Community-IDs können nicht leer sein.",
"Learn more about how we use analytics.": "Lerne mehr darüber, wie wir die Analysedaten nutzen.",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Wenn diese Seite identifizierbare Informationen wie Raum-, Nutzer- oder Gruppen-ID enthält, werden diese Daten entfernt bevor sie an den Server gesendet werden.",
@ -644,7 +644,7 @@
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Um einen Filter zu setzen, ziehe ein Community-Bild auf das Filter-Panel ganz links. Du kannst jederzeit auf einen Avatar im Filter-Panel klicken, um nur die Räume und Personen aus der Community zu sehen.",
"Clear filter": "Filter zurücksetzen",
"Key request sent.": "Schlüssel-Anfragen gesendet.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Wenn du einen Fehler via GitHub gemeldet hast, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Wenn du einen Fehler via GitHub meldest, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer:innen. Sie enthalten keine Nachrichten.",
"Submit debug logs": "Fehlerberichte einreichen",
"Code": "Code",
"Opens the Developer Tools dialog": "Öffnet die Entwickler-Werkzeuge",
@ -759,7 +759,7 @@
"Unhide Preview": "Vorschau wieder anzeigen",
"Unable to join network": "Es ist nicht möglich, dem Netzwerk beizutreten",
"Sorry, your browser is <b>not</b> able to run %(brand)s.": "Es tut uns leid, aber dein Browser kann %(brand)s <b>nicht</b> ausführen.",
"Messages in group chats": "Nachrichten in Gruppen-Chats",
"Messages in group chats": "Nachrichten in Gruppenchats",
"Yesterday": "Gestern",
"Error encountered (%(errorDetail)s).": "Es ist ein Fehler aufgetreten (%(errorDetail)s).",
"Low Priority": "Niedrige Priorität",
@ -802,14 +802,14 @@
"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. <b>This action is irreversible.</b>": "Dies wird deinen Account permanent unbenutzbar machen. Du wirst nicht in der Lage sein, dich anzumelden und keiner wird dieselbe Benutzer-ID erneut registrieren können. Alle Räume, in denen der Account ist, werden verlassen und deine Account-Daten werden vom Identitätsserver gelöscht. <b>Diese Aktion ist unumkehrbar.</b>",
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Standardmäßig werden <b>die von dir gesendeten Nachrichten beim Deaktiveren nicht gelöscht</b>. Wenn du dies von uns möchtest, aktivere das Auswalfeld unten.",
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Die Sichtbarkeit der Nachrichten in Matrix ist vergleichbar mit E-Mails: Wenn wir deine Nachrichten vergessen heißt das, dass diese nicht mit neuen oder nicht registrierten Nutzern teilen werden, aber registrierte Nutzer, die bereits zugriff haben, werden Zugriff auf ihre Kopie behalten.",
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Bitte vergesst alle Nachrichten, die ich gesendet habe, wenn mein Account deaktiviert wird. (<b>Warnung:</b> Zukünftige Nutzer werden eine unvollständige Konversation sehen)",
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Bitte vergesst alle Nachrichten, die ich gesendet habe, wenn mein Account deaktiviert wird. (<b>Warnung:</b> Zukünftige Nutzer:innen werden eine unvollständige Konversation sehen)",
"To continue, please enter your password:": "Um fortzufahren, bitte Passwort eingeben:",
"Can't leave Server Notices room": "Du kannst den Raum für Server-Notizen nicht verlassen",
"This room is used for important messages from the Homeserver, so you cannot leave it.": "Du kannst diesen Raum nicht verlassen, da dieser Raum für wichtige Nachrichten vom Heimserver verwendet wird.",
"Terms and Conditions": "Geschäftsbedingungen",
"To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Um den %(homeserverDomain)s -Heimserver weiter zu verwenden, musst du die Geschäftsbedingungen sichten und ihnen zustimmen.",
"Review terms and conditions": "Geschäftsbedingungen anzeigen",
"Share Link to User": "Link zum Benutzer teilen",
"Share Link to User": "Link zum/r Benutzer:in teilen",
"Share room": "Raum teilen",
"Share Room": "Raum teilen",
"Link to most recent message": "Link zur aktuellsten Nachricht",
@ -819,8 +819,8 @@
"Link to selected message": "Link zur ausgewählten Nachricht",
"COPY": "KOPIEREN",
"Share Message": "Nachricht teilen",
"No Audio Outputs detected": "Keine Ton-Ausgabe erkannt",
"Audio Output": "Ton-Ausgabe",
"No Audio Outputs detected": "Keine Audioausgabe erkannt",
"Audio Output": "Audioausgabe",
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen, wie diesem, ist die Link-Vorschau standardmäßig deaktiviert damit dein Heimserver (auf dem die Vorschau erzeugt wird) keine Informationen über Links in diesem Raum bekommt.",
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Wenn jemand eine URL in seine Nachricht einfügt, kann eine URL-Vorschau angezeigt werden, um mehr Informationen über diesen Link zu erhalten, wie z.B. den Titel, die Beschreibung und ein Bild von der Website.",
"The email field must not be blank.": "Das E-Mail-Feld darf nicht leer sein.",
@ -851,7 +851,7 @@
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Deine Nachricht wurde nicht gesendet, weil dieser Heimserver ein Ressourcen-Limit erreicht hat. Bitte <a>kontaktiere deinen Systemadministrator</a> um diesen Dienst weiter zu nutzen.",
"Please <a>contact your service administrator</a> to continue using this service.": "Bitte <a>kontaktiere deinen Systemadministrator</a> um diesen Dienst weiter zu nutzen.",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, dein Homeserver ist zu alt, um an diesem Raum teilzunehmen.",
"Please contact your homeserver administrator.": "Bitte setze dich mit dem Administrator deines Homeservers in Verbindung.",
"Please contact your homeserver administrator.": "Bitte setze dich mit der Administration deines Homeservers in Verbindung.",
"Legal": "Rechtliches",
"This room has been replaced and is no longer active.": "Dieser Raum wurde ersetzt und ist nicht länger aktiv.",
"The conversation continues here.": "Die Konversation wird hier fortgesetzt.",
@ -865,7 +865,7 @@
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s setzte die Hauptadresse zu diesem Raum auf %(address)s.",
"%(senderName)s removed the main address for this room.": "%(senderName)s entfernte die Hauptadresse von diesem Raum.",
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Bevor du Log-Dateien übermittelst, musst du ein <a>GitHub-Issue erstellen</a> um dein Problem zu beschreiben.",
"%(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 benutzt nun 3-5-mal weniger Arbeitsspeicher, indem Informationen über andere Nutzer erst bei Bedarf geladen werden. Bitte warte, während die Daten erneut mit dem Server abgeglichen werden!",
"%(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 benutzt nun 3 - 5-mal weniger Arbeitsspeicher, indem Informationen über andere Nutzer:innen erst bei Bedarf geladen werden. Bitte warte, während die Daten erneut mit dem Server abgeglichen werden!",
"Updating %(brand)s": "Aktualisiere %(brand)s",
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Du hast zuvor %(brand)s auf %(host)s ohne das verzögerte Laden von Mitgliedern genutzt. In dieser Version war das verzögerte Laden deaktiviert. Da die lokal zwischengespeicherten Daten zwischen diesen Einstellungen nicht kompatibel sind, muss %(brand)s dein Konto neu synchronisieren.",
"If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Wenn %(brand)s mit der alten Version in einem anderen Tab geöffnet ist, schließe dies bitte, da das parallele Nutzen von %(brand)s auf demselben Host mit aktivierten und deaktivierten verzögertem Laden, Probleme verursachen wird.",
@ -873,9 +873,9 @@
"Clear cache and resync": "Zwischenspeicher löschen und erneut synchronisieren",
"Please review and accept the policies of this homeserver:": "Bitte sieh dir alle Bedingungen dieses Heimservers an und akzeptiere sie:",
"Add some now": "Jemanden jetzt hinzufügen",
"You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Du bist ein Administrator dieser Community. Du wirst nicht erneut hinzutreten können, wenn du nicht von einem anderen Administrator eingeladen wirst.",
"Open Devtools": "Öffne Entwickler-Werkzeuge",
"Show developer tools": "Zeige Entwickler-Werkzeuge",
"You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Du bist ein:e Administrator:in dieser Community. Du wirst nicht erneut hinzutreten können, wenn du nicht von einem/r anderen Administrator:in eingeladen wirst.",
"Open Devtools": "Entwicklerwerkzeuge öffnen",
"Show developer tools": "Zeige Entwicklerwerkzeuge",
"Unable to load! Check your network connectivity and try again.": "Konnte nicht geladen werden! Überprüfe die Netzwerkverbindung und versuche es erneut.",
"Delete Backup": "Sicherung löschen",
"Backup version: ": "Sicherungsversion: ",
@ -909,7 +909,7 @@
"All-uppercase is almost as easy to guess as all-lowercase": "Alles groß zu geschrieben ist fast genauso schnell zu raten, wie alles klein zu schreiben",
"Reversed words aren't much harder to guess": "Umgedrehte Worte sind nicht schwerer zu erraten",
"Predictable substitutions like '@' instead of 'a' don't help very much": "Vorhersagbare Ersetzungen wie '@' anstelle von 'a' helfen nicht viel",
"Add another word or two. Uncommon words are better.": "Füge ein weiteres wort hinzu - oder mehr. Ungewöhnliche Worte sind besser.",
"Add another word or two. Uncommon words are better.": "Füge ein weiteres Wort - oder mehr - hinzu. Ungewöhnliche Worte sind besser.",
"Repeats like \"aaa\" are easy to guess": "Wiederholungen wie \"aaa\" sind einfach zu erraten",
"Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Wiederholungen wie \"abcabcabc\" sind nur leicht schwerer zu raten als \"abc\"",
"Sequences like abc or 6543 are easy to guess": "Sequenzen wie \"abc\" oder \"6543\" sind leicht zu raten",
@ -923,12 +923,12 @@
"Names and surnames by themselves are easy to guess": "Namen und Familiennamen alleine sind einfach zu erraten",
"Common names and surnames are easy to guess": "Häufige Namen und Familiennamen sind einfach zu erraten",
"You do not have permission to invite people to this room.": "Du hast keine Berechtigung um Personen in diesen Raum einzuladen.",
"User %(user_id)s does not exist": "Benutzer %(user_id)s existiert nicht",
"Unknown server error": "Unbekannter Server-Fehler",
"Failed to invite users to the room:": "Konnte Benutzer nicht in den Raum einladen:",
"User %(user_id)s does not exist": "Benutzer:in %(user_id)s existiert nicht",
"Unknown server error": "Unbekannter Serverfehler",
"Failed to invite users to the room:": "Konnte Benutzer:innen nicht in den Raum einladen:",
"Short keyboard patterns are easy to guess": "Kurze Tastaturmuster sind einfach zu erraten",
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Zeige eine Erinnerung um die Sichere Nachrichten-Wiederherstellung in verschlüsselten Räumen zu aktivieren",
"Messages containing @room": "Nachrichten die \"@room\" enthalten",
"Messages containing @room": "Nachrichten, die \"@room\" enthalten",
"Encrypted messages in one-to-one chats": "Verschlüsselte Nachrichten in 1:1 Chats",
"Encrypted messages in group chats": "Verschlüsselte Nachrichten in Gruppenchats",
"Use a longer keyboard pattern with more turns": "Nutze ein längeres Tastaturmuster mit mehr Abwechslung",
@ -962,9 +962,9 @@
"Go to Settings": "Gehe zu Einstellungen",
"Sign in with single sign-on": "Melde dich mit „Single Sign-On“ an",
"Unrecognised address": "Nicht erkannte Adresse",
"User %(user_id)s may or may not exist": "Existenz der Benutzer %(user_id)s unsicher",
"Prompt before sending invites to potentially invalid matrix IDs": "Nachfragen bevor Einladungen zu möglichen ungültigen Matrix IDs gesendet werden",
"The following users may not exist": "Eventuell existieren folgende Benutzer nicht",
"User %(user_id)s may or may not exist": "Existenz des/der Benutzers/in %(user_id)s unsicher",
"Prompt before sending invites to potentially invalid matrix IDs": "Nachfragen, bevor Einladungen zu möglichen ungültigen Matrix-IDs gesendet werden",
"The following users may not exist": "Eventuell existieren folgende Benutzer:innen nicht",
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Profile für die unteren Matrix IDs wurden nicht gefunden - willst Du sie trotzdem einladen?",
"Invite anyway and never warn me again": "Trotzdem einladen und mich nicht mehr warnen",
"Invite anyway": "Trotzdem einladen",
@ -989,10 +989,10 @@
"Messages containing my username": "Nachrichten, die meinen Benutzernamen enthalten",
"The other party cancelled the verification.": "Die Gegenstelle hat die Überprüfung abgebrochen.",
"Verified!": "Verifiziert!",
"You've successfully verified this user.": "Du hast diesen Benutzer erfolgreich verifiziert.",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sichere Nachrichten mit diesem Benutzer sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.",
"You've successfully verified this user.": "Du hast diese:n Benutzer:in erfolgreich verifiziert.",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sichere Nachrichten mit diesem/r Benutzer:in sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.",
"Got It": "Verstanden",
"Verify this user by confirming the following number appears on their screen.": "Verifiziere diese Nutzer!n, indem du bestätigst, dass die folgende Nummer auf dessen Bildschirm erscheint.",
"Verify this user by confirming the following number appears on their screen.": "Verifiziere diese Nutzer:in, indem du bestätigst, dass die folgende Nummer auf dessen Bildschirm erscheint.",
"Yes": "Ja",
"No": "Nein",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Wir haben dir eine E-Mail geschickt, um deine Adresse zu überprüfen. Bitte folge den Anweisungen dort und klicke dann auf die Schaltfläche unten.",
@ -1006,9 +1006,9 @@
"Profile picture": "Profilbild",
"Display Name": "Anzeigename",
"Room information": "Rauminformationen",
"Internal room ID:": "Interne Raum ID:",
"Room version": "Raum Version",
"Room version:": "Raum-Version:",
"Internal room ID:": "Interne Raum-ID:",
"Room version": "Raumversion",
"Room version:": "Raumversion:",
"Developer options": "Entwickleroptionen",
"General": "Allgemein",
"Set a new account password...": "Neues Benutzerkonto-Passwort festlegen...",
@ -1031,13 +1031,13 @@
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Die Datei '%(fileName)s' überschreitet die maximale Größe für Uploads auf diesem Heimserver",
"This room has no topic.": "Dieser Raum hat kein Thema.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s machte den Raum für jeden, der den Link kennt öffentlich.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s hat den Raum auf eingeladene User beschränkt.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s hat den Raum auf eingeladene Benutzer beschränkt.",
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s änderte die Zutrittsregel auf '%(rule)s'",
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s erlaubte Gäste diesem Raum beizutreten.",
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s hat Gästen verboten diesem Raum beizutreten.",
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s hat Gästen verboten, diesem Raum beizutreten.",
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s änderte den Gastzugriff auf '%(rule)s'",
"Group & filter rooms by custom tags (refresh to apply changes)": "Gruppiere & filtere Räume nach eigenen Tags (neu laden um Änderungen zu übernehmen)",
"Unable to find a supported verification method.": "Konnte kein unterstützte Verifikationsmethode finden.",
"Unable to find a supported verification method.": "Konnte keine unterstützte Verifikationsmethode finden.",
"Dog": "Hund",
"Cat": "Katze",
"Lion": "Löwe",
@ -1045,7 +1045,7 @@
"Unicorn": "Einhorn",
"Pig": "Schwein",
"Elephant": "Elefant",
"Rabbit": "Kaninchen",
"Rabbit": "Hase",
"Panda": "Panda",
"Rooster": "Hahn",
"Penguin": "Pinguin",
@ -1073,7 +1073,7 @@
"Hat": "Hut",
"Glasses": "Brille",
"Spanner": "Schraubenschlüssel",
"Santa": "Nikolaus",
"Santa": "Weihnachtsmann",
"Thumbs up": "Daumen hoch",
"Umbrella": "Regenschirm",
"Hourglass": "Sanduhr",
@ -1103,7 +1103,7 @@
"Timeline": "Chatverlauf",
"Autocomplete delay (ms)": "Verzögerung zur Autovervollständigung (ms)",
"Roles & Permissions": "Rollen & Berechtigungen",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Änderungen daran, wer den Chatverlauf lesen kann werden nur zukünftige Nachrichten in diesem Raum angewendet. Die Sichtbarkeit der existierenden Historie bleibt unverändert.",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Änderungen daran, wer den Chatverlauf lesen kann werden nur zukünftige Nachrichten in diesem Raum angewendet. Die Sichtbarkeit des existierenden Verlaufs bleibt unverändert.",
"Security & Privacy": "Sicherheit & Datenschutz",
"Encryption": "Verschlüsselung",
"Once enabled, encryption cannot be disabled.": "Sobald aktiviert, kann die Verschlüsselung nicht mehr deaktiviert werden.",
@ -1111,7 +1111,7 @@
"Ignored users": "Ignorierte Benutzer",
"Key backup": "Schlüsselsicherung",
"Gets or sets the room topic": "Frage das Thema des Raums ab oder setze es",
"Verify this user by confirming the following emoji appear on their screen.": "Verifiziere diese Nutzer!n, indem du bestätigst, dass folgendes Emoji auf dessen Bildschirm erscheint.",
"Verify this user by confirming the following emoji appear on their screen.": "Verifiziere diese Nutzer:in, indem du bestätigst, dass folgende Emojis auf dessen Bildschirm erscheinen.",
"Missing media permissions, click the button below to request.": "Fehlende Medienberechtigungen. Drücke auf den Knopf unten, um sie anzufordern.",
"Request media permissions": "Medienberechtigungen anfordern",
"Main address": "Primäre Adresse",
@ -1194,8 +1194,8 @@
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s aktivierte Abzeichen der Gruppen %(groups)s für diesen Raum.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s deaktivierte Abzeichen der Gruppen %(groups)s in diesem Raum.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s aktivierte Abzeichen von %(newGroups)s und deaktivierte die Abzeichen von %(oldGroups)s in diesem Raum.",
"User %(userId)s is already in the room": "Nutzer %(userId)s ist bereits im Raum",
"The user must be unbanned before they can be invited.": "Nutzer müssen entbannt werden, bevor sie eingeladen werden können.",
"User %(userId)s is already in the room": "Nutzer:in %(userId)s ist bereits im Raum",
"The user must be unbanned before they can be invited.": "Die Verbannung des/der Nutzer:in muss aufgehoben werden, bevor er/sie eingeladen werden kann.",
"Show read receipts sent by other users": "Zeige Lesebestätigungen anderer Benutzer",
"Scissors": "Scheren",
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> zu deiner eigenen Domain",
@ -1208,15 +1208,15 @@
"Change topic": "Ändere das Thema",
"Modify widgets": "Ändere Widgets",
"Default role": "Standard Rolle",
"Send messages": "Sende Nachrichten",
"Invite users": "Benutzer einladen",
"Change settings": "Ändere Einstellungen",
"Kick users": "Benutzer kicken",
"Ban users": "Benutzer verbannen",
"Send messages": "Nachrichten senden",
"Invite users": "Benutzer:innen einladen",
"Change settings": "Einstellungen ändern",
"Kick users": "Benutzer:innen kicken",
"Ban users": "Benutzer:innen verbannen",
"Remove messages": "Nachrichten löschen",
"Notify everyone": "Jeden Benachrichtigen",
"Send %(eventType)s events": "Sende %(eventType)s-Ereignisse",
"Select the roles required to change various parts of the room": "Wähle Rollen die benötigt werden um einige Teile des Raumes zu ändern",
"Notify everyone": "Jeden benachrichtigen",
"Send %(eventType)s events": "%(eventType)s-Ereignisse senden",
"Select the roles required to change various parts of the room": "Wähle Rollen, die benötigt werden, um einige Teile des Raumes zu ändern",
"Enable encryption?": "Verschlüsselung aktivieren?",
"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. <a>Learn more about encryption.</a>": "Sobald aktiviert, kann die Verschlüsselung für einen Raum nicht mehr deaktiviert werden. Nachrichten in einem verschlüsselten Raum können nur noch von Teilnehmern aber nicht mehr vom Server gelesen werden. Einige Bots und Brücken werden vielleicht nicht mehr funktionieren. <a>Erfahre mehr über Verschlüsselung.</a>",
"Error updating main address": "Fehler beim Aktualisieren der Hauptadresse",
@ -1235,7 +1235,7 @@
"Your Matrix account on %(serverName)s": "Dein Matrixkonto auf %(serverName)s",
"Name or Matrix ID": "Name oder Matrix ID",
"Your %(brand)s is misconfigured": "Dein %(brand)s ist falsch konfiguriert",
"You cannot modify widgets in this room.": "Du kannst in diesem Raum keine Widgets verändern.",
"You cannot modify widgets in this room.": "Du darfst in diesem Raum keine Widgets verändern.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ob du die \"Breadcrumbs\"-Funktion nutzt oder nicht (Avatare oberhalb der Raumliste)",
"The server does not support the room version specified.": "Der Server unterstützt die angegebene Raumversion nicht.",
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Achtung</b>: Ein Raum-Upgrade wird <i>die Mitglieder des Raumes nicht automatisch auf die neue Version migrieren.</i> Wir werden in der alten Raumversion einen Link zum neuen Raum posten - Raum-Mitglieder müssen dann auf diesen Link klicken um dem neuen Raum beizutreten.",
@ -1243,7 +1243,7 @@
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momentan ist es nicht möglich mit einer Datei zu antworten. Möchtest Du die Datei hochladen ohne zu antworten?",
"The file '%(fileName)s' failed to upload.": "Die Datei \"%(fileName)s\" konnte nicht hochgeladen werden.",
"Changes your avatar in this current room only": "Ändert deinen Avatar für diesen Raum",
"Unbans user with given ID": "Entbannt den Benutzer mit der angegebenen ID",
"Unbans user with given ID": "Hebt die Verbannung des/der Benutzer:in mit der angegebenen ID auf",
"Sends the given message coloured as a rainbow": "Sendet die Nachricht in Regenbogenfarben",
"Adds a custom widget by URL to the room": "Fügt ein Benutzer-Widget über eine URL zum Raum hinzu",
"Please supply a https:// or http:// widget URL": "Bitte gib eine https:// oder http:// Widget-URL an",
@ -1301,9 +1301,9 @@
"Multiple integration managers": "Mehrere Integrationsmanager",
"Public Name": "Öffentlicher Name",
"Identity Server URL must be HTTPS": "Die Identity Server-URL muss HTTPS sein",
"Could not connect to Identity Server": "Verbindung zum Identity Server konnte nicht hergestellt werden",
"Could not connect to Identity Server": "Verbindung zum Identitätsserver konnte nicht hergestellt werden",
"Checking server": "Server wird überprüft",
"Identity server has no terms of service": "Für den Identity Server gelten keine Nutzungsbedingungen",
"Identity server has no terms of service": "Der Identitätsserver hat keine Nutzungsbedingungen",
"Disconnect": "Trennen",
"Identity Server": "Identitätsserver",
"Use an identity server": "Benutze einen Identitätsserver",
@ -1317,7 +1317,7 @@
"Enter a new identity server": "Gib einen neuen Identitätsserver ein",
"Clear personal data": "Persönliche Daten löschen",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Wenn du die Verbindung zu deinem Identitätsserver trennst, heißt das, dass du nicht mehr von anderen Benutzern gefunden werden und auch andere nicht mehr per E-Mail oder Telefonnummer einladen kannst.",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Bitte frage den Administrator deines Heimservers (<code>%(homeserverDomain)s</code>) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Bitte frage die Administration deines Heimservers (<code>%(homeserverDomain)s</code>) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.",
"Disconnect from the identity server <idserver />?": "Verbindung zum Identitätsserver <idserver /> trennen?",
"Add Email Address": "E-Mail-Adresse hinzufügen",
"Add Phone Number": "Telefonnummer hinzufügen",
@ -1328,7 +1328,7 @@
"Find a room…": "Einen Raum suchen…",
"Find a room… (e.g. %(exampleRoom)s)": "Einen Raum suchen… (z.B. %(exampleRoom)s)",
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Wenn du den gesuchten Raum nicht finden kannst, frage nach einer Einladung für den Raum oder <a>Erstelle einen neuen Raum</a>.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter <code>turn.matrix.org</code> zu verwenden. Allerdings wird dieser nicht so zuverlässig sein, und du teilst deine IP-Adresse mit diesem Server. Du kannst dies auch in den Einstellungen konfigurieren.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter <code>turn.matrix.org</code> zu verwenden. Allerdings wird dieser nicht so zuverlässig sein und du teilst deine IP-Adresse mit diesem Server. Du kannst dies auch in den Einstellungen konfigurieren.",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standard-Identitätsserver <server /> zuzugreifen, um eine E-Mail Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.",
"Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du dem/r Besitzer:in des Servers vertraust.",
"Trust": "Vertrauen",
@ -1339,7 +1339,7 @@
"Try out new ways to ignore people (experimental)": "Versuche neue Möglichkeiten, um Menschen zu ignorieren (experimentell)",
"Send read receipts for messages (requires compatible homeserver to disable)": "Lesebestätigungen für Nachrichten senden (Deaktivieren erfordert einen kompatiblen Heimserver)",
"My Ban List": "Meine Bannliste",
"This is your list of users/servers you have blocked - don't leave the room!": "Dies ist die Liste von Benutzer*innen/Servern, die du blockiert hast - verlasse den Raum nicht!",
"This is your list of users/servers you have blocked - don't leave the room!": "Dies ist die Liste von Benutzer:innen und Servern, die du blockiert hast - verlasse diesen Raum nicht!",
"Accept <policyLink /> to continue:": "Akzeptiere <policyLink />, um fortzufahren:",
"Change identity server": "Wechsle den Identitätsserver",
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Du solltest deine <b>persönlichen Daten</b> vom Identitätsserver <idserver /> entfernen, bevor du die Verbindung trennst. Leider ist der Identitätsserver <idserver /> derzeit offline oder kann nicht erreicht werden.",
@ -1370,7 +1370,7 @@
"%(num)s days from now": "in %(num)s Tagen",
"Show info about bridges in room settings": "Information über Bridges in den Raumeinstellungen anzeigen",
"Enable message search in encrypted rooms": "Nachrichtensuche in verschlüsselten Räumen aktivieren",
"Lock": "Sperren",
"Lock": "Schloss",
"Later": "Später",
"Review": "Überprüfen",
"Verify": "Verifizieren",
@ -1384,24 +1384,24 @@
"Cannot connect to integration manager": "Verbindung zum Integrationsmanager fehlgeschlagen",
"The integration manager is offline or it cannot reach your homeserver.": "Der Integrationsmanager ist offline oder er kann den Heimserver nicht erreichen.",
"not stored": "nicht gespeichert",
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Die Sicherung hat eine Signatur von <verify>unbekanntem/r</verify> Nutzer!n mit ID %(deviceId)s",
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Die Sicherung hat eine Signatur von <verify>unbekanntem/r</verify> Nutzer:in mit ID %(deviceId)s",
"Backup key stored: ": "Backup Schlüssel gespeichert: ",
"Clear notifications": "Benachrichtigungen löschen",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Vom Identitätsserver <current /> trennen, und stattdessen eine Verbindung zu <new /> aufbauen?",
"The identity server you have chosen does not have any terms of service.": "Der von dir gewählte Identitätsserver hat keine Nutzungsbedingungen.",
"Disconnect identity server": "Verbindung zum Identitätsserver trennen",
"contact the administrators of identity server <idserver />": "Administrator des Identitätsservers <idserver /> kontaktieren",
"contact the administrators of identity server <idserver />": "Administration des Identitätsservers <idserver /> kontaktieren",
"wait and try again later": "warte und versuche es später erneut",
"Disconnect anyway": "Verbindung trotzdem trennen",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Du <b>teilst deine persönlichen Daten</b> immer noch auf dem Identitätsserver <idserver />.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Wir empfehlen, dass du deine Email Adressen und Telefonnummern vom Identitätsserver löschst, bevor du die Verbindung trennst.",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Zur Zeit benutzt du keinen Identitätsserver. Trage unten einen Server ein, um Kontakte finden und von anderen gefunden zu werden.",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsmanager <b>(%(serverName)s)</b> um Bots, Widgets und Sticker Packs zu verwalten.",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsmanager um Bots, Widgets und Sticker Packs zu verwalten.",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsmanager <b>(%(serverName)s)</b>, um Bots, Widgets und Stickerpacks zu verwalten.",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsmanager, um Bots, Widgets und Sticker Packs zu verwalten.",
"Manage integrations": "Integrationen verwalten",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Stimme den Nutzungsbedingungen des Identitätsservers %(serverName)s zu, um dich per Email Adresse und Telefonnummer auffindbar zu machen.",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Stimme den Nutzungsbedingungen des Identitätsservers %(serverName)s zu, um dich per E-Mail-Adresse und Telefonnummer auffindbar zu machen.",
"Clear cache and reload": "Zwischenspeicher löschen und neu laden",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Passe deine Erfahrung mit experimentellen Lab Funktionen an. <a>Mehr erfahren</a>.",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Passe deine Erfahrung mit experimentellen Funktionen an. <a>Mehr erfahren</a>.",
"Ignored/Blocked": "Ignoriert/Blockiert",
"Something went wrong. Please try again or view your console for hints.": "Etwas ist schief gelaufen. Bitte versuche es erneut oder sieh für weitere Hinweise in deiner Konsole nach.",
"Error subscribing to list": "Fehler beim Abonnieren der Liste",
@ -1413,10 +1413,10 @@
"You have not ignored anyone.": "Du hast niemanden ignoriert.",
"You are currently ignoring:": "Du ignorierst momentan:",
"Unsubscribe": "Deabonnieren",
"View rules": "Regeln betrachten",
"View rules": "Regeln öffnen",
"You are currently subscribed to:": "Du abonnierst momentan:",
"⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.",
"Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Berührung der primäre Eingabemechanismus ist",
"⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer:innen gedacht.",
"Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Touch das primäre Eingabegerät ist",
"Whether you're using %(brand)s as an installed Progressive Web App": "Ob du %(brand)s als installierte progressive Web-App verwendest",
"Your user agent": "Dein User-Agent",
"If you cancel now, you won't complete verifying the other user.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung des anderen Nutzers nicht beenden können.",
@ -1427,14 +1427,14 @@
"Verifies a user, session, and pubkey tuple": "Verifiziert einen Benutzer, eine Sitzung und Pubkey-Tupel",
"Unknown (user, session) pair:": "Unbekanntes (Nutzer-, Sitzungs-) Paar:",
"Session already verified!": "Sitzung bereits verifiziert!",
"WARNING: Session already verified, but keys do NOT MATCH!": "ACHTUNG: Sitzung bereits verifiziert, aber die Schlüssel passen NICHT ZUSAMMEN!",
"WARNING: Session already verified, but keys do NOT MATCH!": "WARNUNG: Die Sitzung wurde bereits verifiziert, aber die Schlüssel passen NICHT ZUSAMMEN!",
"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!": "ACHTUNG: SCHLÜSSEL-VERIFIZIERUNG FEHLGESCHLAGEN! Der Signierschlüssel für %(userId)s und Sitzung %(deviceId)s ist \"%(fprint)s\", was nicht mit dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmt. Das könnte bedeuten, dass deine Kommunikation abgehört wird!",
"Never send encrypted messages to unverified sessions from this session": "Sende niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen",
"Never send encrypted messages to unverified sessions in this room from this session": "Sende niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen in diesem Raum",
"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.": "Durch die Änderung des Passworts werden derzeit alle Ende-zu-Ende-Verschlüsselungsschlüssel in allen Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist, es sei denn, du exportierst zuerst deine Raumschlüssel und importierst sie anschließend wieder. In Zukunft wird dies verbessert werden.",
"Delete %(count)s sessions|other": "Lösche %(count)s Sitzungen",
"Delete %(count)s sessions|other": "%(count)s Sitzungen löschen",
"Backup is not signed by any of your sessions": "Die Sicherung wurde von keiner deiner Sitzungen unterzeichnet",
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhälst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldst",
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhältst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldest",
"Notification sound": "Benachrichtigungston",
"Set a new custom sound": "Setze einen neuen benutzerdefinierten Ton",
"Browse": "Durchsuche",
@ -1454,10 +1454,10 @@
"Go Back": "Zurückgehen",
"Notification Autocomplete": "Benachrichtigung Autovervollständigen",
"If disabled, messages from encrypted rooms won't appear in search results.": "Wenn deaktiviert, werden Nachrichten von verschlüsselten Räumen nicht in den Ergebnissen auftauchen.",
"This user has not verified all of their sessions.": "Dieser Benutzer hat nicht alle seine Sitzungen verifiziert.",
"You have verified this user. This user has verified all of their sessions.": "Du hast diese/n Nutzer!n verifiziert. Er/Sie hat alle seine/ihre Sitzungen verifiziert.",
"This user has not verified all of their sessions.": "Diese:r Benutzer:in hat nicht alle seine/ihre Sitzungen verifiziert.",
"You have verified this user. This user has verified all of their sessions.": "Du hast diese/n Nutzer:in verifiziert. Er/Sie hat alle seine/ihre Sitzungen verifiziert.",
"Your key share request has been sent - please check your other sessions for key share requests.": "Deine Schlüsselanfrage wurde gesendet - sieh in deinen anderen Sitzungen nach der Schlüsselanfrage.",
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast klicke hier, um die Schlüssel erneut anzufordern.",
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast, klicke hier, um die Schlüssel erneut anzufordern.",
"If your other sessions do not have the key for this message you will not be able to decrypt them.": "Wenn deine anderen Sitzungen nicht über den Schlüssel für diese Nachricht verfügen, kannst du die Nachricht nicht entschlüsseln.",
"<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Fordere die Verschlüsselungsschlüssel aus deinen anderen Sitzungen erneut an</requestLink>.",
"Room %(name)s": "Raum %(name)s",
@ -1490,12 +1490,12 @@
"Use bots, bridges, widgets and sticker packs": "Benutze Bots, Bridges, Widgets und Sticker-Packs",
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Wenn du dein Passwort änderst, werden alle Ende-zu-Ende-Verschlüsselungsschlüssel für alle deine Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist. Richte ein Schlüssel-Backup ein oder exportiere deine Raumschlüssel aus einer anderen Sitzung, bevor du dein Passwort zurücksetzst.",
"You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Du wurdest von allen Sitzungen abgemeldet und erhälst keine Push-Benachrichtigungen mehr. Um die Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an.",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aktualisiere diese Sitzung, damit sie andere Sitzungen verifizieren kann, indem sie dir Zugang zu verschlüsselten Nachrichten gewährt und sie für andere Benutzer als vertrauenswürdig markiert.",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aktualisiere diese Sitzung, damit sie andere Sitzungen verifizieren kann, indem sie dir Zugang zu verschlüsselten Nachrichten gewährt und sie für andere Benutzer:innen als vertrauenswürdig markiert.",
"Sign out and remove encryption keys?": "Abmelden und Verschlüsselungsschlüssel entfernen?",
"Sign in to your Matrix account on <underlinedServerName />": "Melde dich bei deinem Matrix-Konto auf <underlinedServerName /> an",
"Enter your password to sign in and regain access to your account.": "Gib dein Passwort ein, um dich anzumelden und wieder Zugang zu deinem Konto zu erhalten.",
"Sign in and regain access to your account.": "Melden dich an und erhalte wieder Zugang zu deinem Konto.",
"You cannot sign in to your account. Please contact your homeserver admin for more information.": "Du kannst dich nicht bei deinem Konto anmelden. Bitte kontaktiere deinen Homeserver-Administrator für weitere Informationen.",
"You cannot sign in to your account. Please contact your homeserver admin for more information.": "Du kannst dich nicht bei deinem Konto anmelden. Bitte kontaktiere deine Homeserver-Administration für weitere Informationen.",
"Sign In or Create Account": "Anmelden oder Account erstellen",
"Use your account or create a new one to continue.": "Benutze deinen Account oder erstellen einen neuen, um fortzufahren.",
"Create Account": "Account erstellen",
@ -1568,7 +1568,7 @@
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s hat die alternativen Adressen %(addresses)s für diesen Raum entfernt.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s hat die alternative Adresse %(addresses)s für diesen Raum entfernt.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s hat die alternative Adresse für diesen Raum geändert.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s hat die Haupt- und Alternativadresse für diesen Raum geändert.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s hat die Haupt- und Alternativadressen für diesen Raum geändert.",
"%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Nutzer!nnen, die %(glob)s entsprechen",
"%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Räume, die %(glob)s entsprechen",
"%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Server, die %(glob)s entsprechen",
@ -1589,12 +1589,12 @@
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel für Räume von %(oldGlob)s nach %(newGlob)s, wegen %(reason)s",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Auf den Server turn.matrix.org zurückgreifen, falls deine Heimserver keine Anruf-Assistenz anbietet (deine IP-Adresse wird während eines Anrufs geteilt)",
"Show more": "Mehr zeigen",
"This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Diese Sitzung <b>sichert nicht deine Schlüssel</b>, aber du hast eine vorhandene Sicherung, die du wiederherstellen und in Zukunft hinzufügen kannst.",
"This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Diese Sitzung <b>sichert deine Schlüssel nicht</b>, aber du hast eine vorhandene Sicherung, die du wiederherstellen und in Zukunft hinzufügen kannst.",
"Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Verbinde diese Sitzung mit deiner Schlüsselsicherung bevor du dich abmeldest, um den Verlust von Schlüsseln zu vermeiden.",
"This backup is trusted because it has been restored on this session": "Dieser Sicherung wird vertraut, da sie während dieser Sitzung wiederhergestellt wurde",
"Enable desktop notifications for this session": "Desktop-Benachrichtigungen für diese Sitzung aktivieren",
"Enable audible notifications for this session": "Aktiviere die akustischen Benachrichtigungen für diese Sitzung",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsmanaager erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Einflusslevel setzen.",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsmanager erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Berechtigungslevel setzen.",
"Read Marker lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung (ms)",
"Read Marker off-screen lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung außerhalb des Bildschirms (ms)",
"Session key:": "Sitzungsschlüssel:",
@ -1643,9 +1643,9 @@
"%(name)s is requesting verification": "%(name)s fordert eine Verifizierung an",
"Failed to set topic": "Das Festlegen des Themas ist fehlgeschlagen",
"Command failed": "Befehl fehlgeschlagen",
"Could not find user in room": "Der Benutzer konnte im Raum nicht gefunden werden",
"Could not find user in room": "Benutzer:in 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.",
"Confirm adding phone number": "Bestätige hinzugefügte Telefonnummer",
"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 Ausschluss-Regel 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 Ausschluss-Regel von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
"Not Trusted": "Nicht vertraut",
@ -1679,8 +1679,8 @@
"Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Bestätige das Löschen dieser Sitzung indem du dich mittels Single Sign-On anmeldest um deine Identität nachzuweisen.",
"Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Bestätige das Löschen dieser Sitzung indem du dich mittels Single Sign-On anmeldest um deine Identität nachzuweisen.",
"Confirm deleting these sessions": "Bestätige das Löschen dieser Sitzungen",
"Click the button below to confirm deleting these sessions.|other": "Klicke den Button um das Löschen dieser Sitzungen zu bestätigen.",
"Click the button below to confirm deleting these sessions.|one": "Klicke den Button um das Löschen dieser Sitzung zu bestätigen.",
"Click the button below to confirm deleting these sessions.|other": "Klicke den Knopf, um das Löschen dieser Sitzungen zu bestätigen.",
"Click the button below to confirm deleting these sessions.|one": "Klicke den Knopf, um das Löschen dieser Sitzung zu bestätigen.",
"Clear all data in this session?": "Alle Daten dieser Sitzung löschen?",
"Clear all data": "Alle Daten löschen",
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Bestätige das Löschen deines Kontos indem du dich mittels Single Sign-On anmeldest um deine Identität nachzuweisen.",
@ -1702,15 +1702,15 @@
"Error adding ignored user/server": "Fehler beim Hinzufügen eines ignorierten Nutzers/Servers",
"None": "Keine",
"Ban list rules - %(roomName)s": "Verbotslistenregeln - %(roomName)s",
"Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Füge hier Benutzer!nnen und Server hinzu, die du ignorieren willst. Verwende Sternchen, damit %(brand)s mit beliebigen Zeichen übereinstimmt. Bspw. würde <code>@bot: *</code> alle Benutzer!nnen ignorieren, die auf einem Server den Namen 'bot' haben.",
"Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorieren von Personen erfolgt über Sperrlisten. Wenn eine Sperrliste abonniert wird, werden die von dieser Liste blockierten Benutzer!nnen/Server ausgeblendet.",
"Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Füge hier Benutzer!nnen und Server hinzu, die du ignorieren willst. Verwende Sternchen, damit %(brand)s mit beliebigen Zeichen übereinstimmt. Bspw. würde <code>@bot: *</code> alle Benutzer:innen ignorieren, die auf einem Server den Namen 'bot' haben.",
"Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Das Ignorieren von Personen erfolgt über Sperrlisten. Wenn eine Sperrliste abonniert wird, werden die von dieser Liste blockierten Benutzer:innen und Server ausgeblendet.",
"Personal ban list": "Persönliche Sperrliste",
"Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Deine persönliche Sperrliste enthält alle Benutzer!nnen/Server, von denen du persönlich keine Nachrichten sehen willst. Nachdem du den ersten Benutzer/Server ignoriert hast, wird in der Raumliste \"Meine Sperrliste\" angezeigt - bleibe in diesem Raum, um die Sperrliste aufrecht zu halten.",
"Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Deine persönliche Sperrliste enthält alle Benutzer:innen/Server, von denen du persönlich keine Nachrichten sehen willst. Nachdem du den ersten Benutzer/Server ignoriert hast, wird in der Raumliste \"Meine Sperrliste\" angezeigt - bleibe in diesem Raum, um die Sperrliste aufrecht zu halten.",
"Server or user ID to ignore": "Zu ignorierende Server- oder Benutzer-ID",
"eg: @bot:* or example.org": "z.B. @bot:* oder example.org",
"Subscribed lists": "Abonnierte Listen",
"Subscribing to a ban list will cause you to join it!": "Eine Verbotsliste abonnieren bedeutet ihr beizutreten!",
"If this isn't what you want, please use a different tool to ignore users.": "Wenn dies nicht das ist, was du willst, verwende ein anderes Tool, um Benutzer!nnen zu ignorieren.",
"If this isn't what you want, please use a different tool to ignore users.": "Wenn dies nicht das ist, was du willst, verwende ein anderes Tool, um Benutzer:innen zu ignorieren.",
"Subscribe": "Abonnieren",
"Always show the window menu bar": "Fenstermenüleiste immer anzeigen",
"Show tray icon and minimize window to it on close": "Taskleistensymbol anzeigen und Fenster beim Schließen dorthin minimieren",
@ -1728,7 +1728,7 @@
"Verify all your sessions to ensure your account & messages are safe": "Verifiziere alle deine Sitzungen, um dein Konto und deine Nachrichten zu schützen",
"Verify your other session using one of the options below.": "Verifiziere deine andere Sitzung mit einer der folgenden Optionen.",
"You signed in to a new session without verifying it:": "Du hast dich in einer neuen Sitzung angemeldet ohne sie zu verifizieren:",
"Other users may not trust it": "Andere Benutzer vertrauen ihr vielleicht nicht",
"Other users may not trust it": "Andere Benutzer:innen vertrauen ihr vielleicht nicht",
"Upgrade": "Hochstufen",
"Verify the new login accessing your account: %(name)s": "Verifiziere die neue Anmeldung an deinem Konto: %(name)s",
"From %(deviceName)s (%(deviceId)s)": "Von %(deviceName)s (%(deviceId)s)",
@ -1752,8 +1752,8 @@
"in account data": "in den Kontodaten",
"Homeserver feature support:": "Home-Server-Funktionsunterstützung:",
"exists": "existiert",
"Delete sessions|other": "Lösche Sitzungen",
"Delete sessions|one": "Lösche Sitzung",
"Delete sessions|other": "Sitzungen löschen",
"Delete sessions|one": "Sitzung löschen",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Alle Sitzungen einzeln verifizieren, anstatt auch Sitzungen zu vertrauen, die durch Cross-Signing verifiziert sind.",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Der Zwischenspeicher für die lokale Suche in verschlüsselten Nachrichten benötigt ",
" to store messages from ": " um Nachrichten von ",
@ -1766,7 +1766,7 @@
"Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "Die Sicherung hat eine <validity>ungültige</validity> Signatur von einer <verify>nicht verifizierten</verify> Sitzung <device></device>",
"Your keys are <b>not being backed up from this session</b>.": "Deine Schlüssel werden <b>nicht von dieser Sitzung gesichert</b>.",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Zur Zeit verwendest du <server></server>, um Kontakte zu finden und von anderen gefunden zu werden. Du kannst deinen Identitätsserver weiter unten ändern.",
"Invalid theme schema.": "Ungültiges Design Schema.",
"Invalid theme schema.": "Ungültiges Designschema.",
"Error downloading theme information.": "Fehler beim herunterladen des Themas.",
"Theme added!": "Design hinzugefügt!",
"Custom theme URL": "URL des benutzerdefinierten Designs",
@ -1781,11 +1781,11 @@
"Complete": "Abschließen",
"Revoke": "Widerrufen",
"Share": "Teilen",
"You have not verified this user.": "Du hast diese:n Nutzer!n nicht verifiziert.",
"Everyone in this room is verified": "Jede/r in diesem Raum ist verifiziert",
"You have not verified this user.": "Du hast diese:n Nutzer:in nicht verifiziert.",
"Everyone in this room is verified": "Alle in diesem Raum sind verifiziert",
"Mod": "Mod",
"Invite only": "Nur auf Einladung",
"Scroll to most recent messages": "Springe zur neusten Nachricht",
"Scroll to most recent messages": "Zur neusten Nachricht springen",
"No recent messages by %(user)s found": "Keine neuen Nachrichten von %(user)s gefunden",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Versuche nach oben zu scrollen, um zu sehen ob sich dort frühere Nachrichten befinden.",
"For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Dies kann bei vielen Nachrichten einige Zeit dauern. Bitte lade die Anwendung in dieser Zeit nicht neu.",
@ -1841,7 +1841,7 @@
"No other published addresses yet, add one below": "Keine anderen öffentlichen Adressen vorhanden, füge unten eine hinzu",
"New published address (e.g. #alias:server)": "Neue öffentliche Adresse (z.B. #alias:server)",
"Local Addresses": "Lokale Adressen",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Erstelle Adressen für diesen Raum, damit andere Benutzer den Raum auf deinem Heimserver (%(localDomain)s) finden können",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Erstelle Adressen für diesen Raum, damit andere Nutzer:innen den Raum auf deinem Heimserver (%(localDomain)s) finden können",
"Waiting for you to accept on your other session…": "Warte auf die Bestätigung in deiner anderen Sitzung…",
"Waiting for %(displayName)s to accept…": "Warte auf die Annahme von %(displayName)s …",
"Accepting…": "Annehmen…",
@ -1861,8 +1861,8 @@
"This client does not support end-to-end encryption.": "Diese Anwendung unterstützt keine Ende-zu-Ende-Verschlüsselung.",
"Verify by scanning": "Verifizierung durch QR-Code-Scannen",
"If you can't scan the code above, verify by comparing unique emoji.": "Wenn du den obigen Code nicht scannen kannst, verifiziere stattdessen durch den Emoji-Vergleich.",
"Verify all users in a room to ensure it's secure.": "Verifiziere alle Benutzer in einem Raum um die vollständige Sicherheit zu gewährleisten.",
"In encrypted rooms, verify all users to ensure its secure.": "Verifiziere alle Benutzer in verschlüsselten Räumen um die vollständige Sicherheit zu gewährleisten.",
"Verify all users in a room to ensure it's secure.": "Verifiziere alle Benutzer:innen in einem Raum um die vollständige Sicherheit zu gewährleisten.",
"In encrypted rooms, verify all users to ensure its secure.": "Verifiziere alle Benutzer:innen in verschlüsselten Räumen um die vollständige Sicherheit zu gewährleisten.",
"You've successfully verified %(deviceName)s (%(deviceId)s)!": "Du hast %(deviceName)s (%(deviceId)s) erfolgreich verifiziert!",
"Verified": "Verifiziert",
"Start verification again from the notification.": "Starte die Verifikation aus der Benachrichtigung erneut.",
@ -2052,8 +2052,8 @@
"Continue with previous account": "Mit vorherigen Konto fortfahren",
"<a>Log in</a> to your new account.": "Mit deinem neuen Konto <a>anmelden</a>.",
"You can now close this window or <a>log in</a> to your new account.": "Du kannst dieses Fenster jetzt schließen oder dich mit deinem neuen Konto <a>anmelden</a>.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer sehen sie als vertrauenswürdig an.",
"Your new session is now verified. Other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Andere Benutzer sehen sie als vertrauenswürdig an.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer:innen sehen sie als vertrauenswürdig an.",
"Your new session is now verified. Other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Andere Benutzer:innen sehen sie als vertrauenswürdig an.",
"well formed": "wohlgeformt",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Wenn du <server /> nicht verwenden willst, um Kontakte zu finden und von anderen gefunden zu werden, trage unten einen anderen Identitätsserver ein.",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Um ein Matrix-bezogenes Sicherheitsproblem zu melden, lies bitte die Matrix.org <a>Sicherheitsrichtlinien</a>.",
@ -2074,7 +2074,7 @@
"Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Verifiziere dieses Gerät und es wird es als vertrauenswürdig markiert. Benutzer, die sich bei dir verifiziert haben, werden diesem Gerät auch vertrauen.",
"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.",
"We couldn't create your DM. Please check the users you want to invite and try again.": "Wir konnten deine Direktnachricht nicht erstellen. Bitte überprüfe den Benutzer, den du einladen möchtest, und versuche es erneut.",
"We couldn't invite those users. Please check the users you want to invite and try again.": "Wir konnten diese Benutzer nicht einladen. Bitte überprüfe sie und versuche es erneut.",
"We couldn't invite those users. Please check the users you want to invite and try again.": "Wir konnten diese Benutzer:innen nicht einladen. Bitte überprüfe sie und versuche es erneut.",
"Start a conversation with someone using their name, username (like <userId/>) or email address.": "Starte eine Unterhaltung mit jemandem indem du seinen Namen, Benutzernamen (z.B. <userId/>) oder E-Mail-Adresse eingibst.",
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Lade jemanden mit seinem Namen, Benutzernamen (z.B. <userId/>) oder E-Mail-Adresse ein oder <a>teile diesen Raum</a>.",
"Upload completed": "Hochladen abgeschlossen",
@ -2096,7 +2096,7 @@
"Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "Die Sicherung konnte nicht mit dem angegebenen Wiederherstellungsschlüssel entschlüsselt werden: Bitte überprüfe ob du den richtigen Wiederherstellungsschlüssel eingegeben hast.",
"Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Die Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden: Bitte überprüfe ob du die richtige Wiederherstellungspassphrase eingegeben hast.",
"Nice, strong password!": "Super, ein starkes Passwort!",
"Other users can invite you to rooms using your contact details": "Andere Benutzer können dich mit deinen Kontaktdaten in Räume einladen",
"Other users can invite you to rooms using your contact details": "Andere Benutzer:innen können dich mit deinen Kontaktdaten in Räume einladen",
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Lege eine E-Mail für die Kontowiederherstellung fest. Verwende optional E-Mail oder Telefon, um von Anderen gefunden zu werden.",
"Explore Public Rooms": "Öffentliche Räume erkunden",
"If you've joined lots of rooms, this might take a while": "Du bist einer Menge Räumen beigetreten, das kann eine Weile dauern",
@ -2136,7 +2136,7 @@
"Jump to room search": "Springe zur Raumsuche",
"Close dialog or context menu": "Schließe Dialog oder Kontextmenü",
"Cancel autocomplete": "Deaktiviere Auto-Vervollständigung",
"Unable to revoke sharing for email address": "Das Teilen der E-Mail-Adresse kann nicht widerrufen werden",
"Unable to revoke sharing for email address": "Dem Teilen der E-Mail-Adresse kann nicht widerrufen werden",
"Unable to validate homeserver/identity server": "Heimserver/Identitätsserver nicht validierbar",
"Without completing security on this session, it wont have access to encrypted messages.": "Ohne Abschluss der Sicherungseinrichtung in dieser Sitzung wird sie keinen Zugriff auf verschlüsselte Nachrichten erhalten.",
"Disable": "Deaktivieren",
@ -2174,7 +2174,7 @@
"Toggle this dialog": "Diesen Dialog ein-/ausblenden",
"Move autocomplete selection up/down": "Auto-Vervollständigung nach oben/unten verschieben",
"Opens chat with the given user": "Öffnet einen Chat mit diesem Benutzer",
"Sends a message to the given user": "Sendet diesem Benutzer eine Nachricht",
"Sends a message to the given user": "Sendet diesem/r Benutzer:in eine Nachricht",
"Waiting for your other session to verify…": "Warte auf die Verifikation deiner anderen Sitzungen…",
"You've successfully verified your device!": "Du hast dein Gerät erfolgreich verifiziert!",
"QR Code": "QR-Code",
@ -2202,7 +2202,7 @@
"I want to help": "Ich möchte helfen",
"Your homeserver has exceeded its user limit.": "Dein Heimserver hat das Benutzerlimit erreicht.",
"Your homeserver has exceeded one of its resource limits.": "Dein Heimserver hat eine seiner Ressourcengrenzen erreicht.",
"Contact your <a>server admin</a>.": "Kontaktiere deinen <a>Heimserver Administrator</a>.",
"Contact your <a>server admin</a>.": "Kontaktiere deine <a>Heimserver-Administration</a>.",
"Ok": "Ok",
"Set password": "Setze Passwort",
"To return to your account in future you need to set a password": "Um dein Konto zukünftig wieder verwenden zu können, setze ein Passwort",
@ -2225,7 +2225,7 @@
"Address (optional)": "Adresse (optional)",
"delete the address.": "lösche die Adresse.",
"Use a different passphrase?": "Eine andere Passphrase verwenden?",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Dein Server-Administrator hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Deine Server-Administration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.",
"People": "Personen",
"There was an error removing that address. It may no longer exist or a temporary error occurred.": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert diese nicht mehr oder es kam zu einem temporären Fehler.",
"Set a room address to easily share your room with other people.": "Vergebe eine Raum-Adresse, um diesen Raum auf einfache Weise mit anderen Personen teilen zu können.",
@ -2333,7 +2333,7 @@
"Incoming voice call": "Eingehender Sprachanruf",
"Incoming video call": "Eingehender Videoanruf",
"Incoming call": "Eingehender Anruf",
"There are advanced notifications which are not shown here.": "Erweiterte Benachrichtigungen, werden hier nicht angezeigt.",
"There are advanced notifications which are not shown here.": "Erweiterte Benachrichtigungen werden hier nicht angezeigt.",
"Are you sure you want to cancel entering passphrase?": "Bist du sicher, dass du die Eingabe der Passphrase abbrechen möchtest?",
"Use your account to sign in to the latest version": "Melde dich mit deinem Account in der neuesten Version an",
"* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
@ -2378,7 +2378,7 @@
"A connection error occurred while trying to contact the server.": "Beim Versuch, den Server zu kontaktieren, ist ein Verbindungsfehler aufgetreten.",
"You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Du hast sie ggf. in einem anderen Client als %(brand)s konfiguriert. Du kannst sie nicht in %(brand)s verändern, aber sie werden trotzdem angewandt.",
"Master private key:": "Privater Hauptschlüssel:",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart & %(brand)s werden versuchen sie zu verwenden.",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart & %(brand)s wird versuchen, sie zu verwenden.",
"Custom Tag": "Benutzerdefinierter Tag",
"Youre already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at <a>element.io/get-started</a>.": "Du bist bereits eingeloggt und kannst loslegen. Allerdings kannst du auch die neuesten Versionen der App für alle Plattformen unter <a>element.io/get-started</a> herunterladen.",
"You're all caught up.": "Alles gesichtet.",
@ -2557,8 +2557,8 @@
"Places the call in the current room on hold": "Den aktuellen Anruf halten",
"Uzbekistan": "Usbekistan",
"Send stickers into this room": "Stickers in diesen Raum senden",
"Send stickers into your active room": "Stickers in deinen aktiven Raum senden",
"Change which room you're viewing": "Ändern welchen Raum du siehst",
"Send stickers into your active room": "Sticker in deinen aktiven Raum senden",
"Change which room you're viewing": "Ändern, welchen Raum du siehst",
"Change the topic of this room": "Das Thema von diesem Raum ändern",
"See when the topic changes in this room": "Sehen wenn sich das Thema in diesem Raum ändert",
"Change the topic of your active room": "Das Thema von deinem aktiven Raum ändern",
@ -2574,7 +2574,7 @@
"Send stickers to this room as you": "Einen Sticker in diesen Raum als du senden",
"See when a sticker is posted in this room": "Sehe wenn ein Sticker in diesen Raum gesendet wird",
"Send stickers to your active room as you": "Einen Sticker als du in deinen aktiven Raum senden",
"See when anyone posts a sticker to your active room": "Sehen wenn jemand einen Sticker in deinen aktiven Raum sendet",
"See when anyone posts a sticker to your active room": "Sehen, wenn jemand einen Sticker in deinen aktiven Raum sendet",
"with an empty state key": "mit einem leeren Zustandsschlüssel",
"with state key %(stateKey)s": "mit Zustandsschlüssel %(stateKey)s",
"Send <b>%(eventType)s</b> events as you in this room": "<b>%(eventType)s</b>-Events als du in diesem Raum senden",
@ -2583,33 +2583,33 @@
"See <b>%(eventType)s</b> events posted to your active room": "In deinem aktiven Raum gesendete <b>%(eventType)s</b>-Events anzeigen",
"The <b>%(capability)s</b> capability": "Die <b>%(capability)s</b> Fähigkeit",
"Send messages as you in this room": "Nachrichten als du in diesem Raum senden",
"Send messages as you in your active room": "Eine Nachricht als du in deinem aktiven Raum senden",
"See messages posted to this room": "In diesem Raum gesendete Nachrichten anzeigen",
"See messages posted to your active room": "In deinem aktiven Raum gesendete Nachrichten anzeigen",
"Send text messages as you in this room": "Textnachrichten als du in diesem Raum senden",
"Send text messages as you in your active room": "Textnachrichten als du in deinem aktiven Raum senden",
"See text messages posted to this room": "In diesem Raum gesendete Textnachrichten anzeigen",
"See text messages posted to your active room": "In deinem aktiven Raum gesendete Textnachrichten anzeigen",
"Send emotes as you in this room": "Emojis als du in diesem Raum senden",
"Send emotes as you in your active room": "Emojis als du in deinem aktiven Raum senden",
"Send messages as you in your active room": "Eine Nachricht als du in deinen aktiven Raum senden",
"See messages posted to this room": "In diesen Raum gesendete Nachrichten anzeigen",
"See messages posted to your active room": "In deinen aktiven Raum gesendete Nachrichten anzeigen",
"Send text messages as you in this room": "Textnachrichten als du in diesen Raum senden",
"Send text messages as you in your active room": "Textnachrichten als du in deinen aktiven Raum senden",
"See text messages posted to this room": "In diesen Raum gesendete Textnachrichten anzeigen",
"See text messages posted to your active room": "In deinen aktiven Raum gesendete Textnachrichten anzeigen",
"Send emotes as you in this room": "Emojis als du in diesen Raum senden",
"Send emotes as you in your active room": "Emojis als du in deinen aktiven Raum senden",
"See emotes posted to this room": "In diesem Raum gesendete Emojis anzeigen",
"See emotes posted to your active room": "In deinem aktiven Raum gesendete Emojis anzeigen",
"See videos posted to your active room": "In deinem aktiven Raum gesendete Videos anzeigen",
"See videos posted to this room": "In diesem Raum gesendete Videos anzeigen",
"Send images as you in this room": "Bilder als du in diesem Raum senden",
"See emotes posted to your active room": "In deinen aktiven Raum gesendete Emojis anzeigen",
"See videos posted to your active room": "In deinen aktiven Raum gesendete Videos anzeigen",
"See videos posted to this room": "In diesen Raum gesendete Videos anzeigen",
"Send images as you in this room": "Bilder als du in diesen Raum senden",
"Send images as you in your active room": "Bilder als du in deinem aktiven Raum senden",
"See images posted to this room": "In diesem Raum gesendete Bilder anzeigen",
"See images posted to your active room": "In deinem aktiven Raum gesendete Bilder anzeigen",
"Send videos as you in this room": "Videos als du in diesem Raum senden",
"Send videos as you in your active room": "Videos als du in deinem aktiven Raum senden",
"Send general files as you in this room": "Allgemeine Dateien als du in diesem Raum senden",
"Send general files as you in your active room": "Allgemeine Dateien als du in deinem aktiven Raum senden",
"See general files posted to your active room": "Allgemeine in deinem aktiven Raum gesendete Dateien anzeigen",
"See general files posted to this room": "Allgemeine in diesem Raum gesendete Dateien anzeigen",
"Send <b>%(msgtype)s</b> messages as you in this room": "Sende <b>%(msgtype)s</b> Nachrichten als du in diesem Raum",
"Send <b>%(msgtype)s</b> messages as you in your active room": "Sende <b>%(msgtype)s</b> Nachrichten als du in deinem aktiven Raum",
"See <b>%(msgtype)s</b> messages posted to this room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in diesem Raum gesendet worden sind",
"See <b>%(msgtype)s</b> messages posted to your active room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in deinem aktiven Raum gesendet worden sind",
"See images posted to this room": "In diesen Raum gesendete Bilder anzeigen",
"See images posted to your active room": "In deinen aktiven Raum gesendete Bilder anzeigen",
"Send videos as you in this room": "Videos als du in diesen Raum senden",
"Send videos as you in your active room": "Videos als du in deinen aktiven Raum senden",
"Send general files as you in this room": "Allgemeine Dateien als du in diesen Raum senden",
"Send general files as you in your active room": "Allgemeine Dateien als du in deinen aktiven Raum senden",
"See general files posted to your active room": "Allgemeine in deinen aktiven Raum gesendete Dateien anzeigen",
"See general files posted to this room": "Allgemeine in diesen Raum gesendete Dateien anzeigen",
"Send <b>%(msgtype)s</b> messages as you in this room": "Sende <b>%(msgtype)s</b> Nachrichten als du in diesen Raum",
"Send <b>%(msgtype)s</b> messages as you in your active room": "Sende <b>%(msgtype)s</b> Nachrichten als du in deinen aktiven Raum",
"See <b>%(msgtype)s</b> messages posted to this room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in diesen Raum gesendet worden sind",
"See <b>%(msgtype)s</b> messages posted to your active room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in deinen aktiven Raum gesendet worden sind",
"Don't miss a reply": "Verpasse keine Antwort",
"Enable desktop notifications": "Aktiviere Desktopbenachrichtigungen",
"Update %(brand)s": "Aktualisiere %(brand)s",
@ -2619,8 +2619,8 @@
"Use Command + Enter to send a message": "Benutze Betriebssystemtaste + Enter um eine Nachricht zu senden",
"Use Ctrl + Enter to send a message": "Benutze Strg + Enter um eine Nachricht zu senden",
"Call Paused": "Anruf pausiert",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern um sie in Suchergebnissen finden zu können, benötigt %(size)s um die Nachrichten von den Räumen %(rooms)s zu speichern.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern um sie in Suchergebnissen finden zu können, benötigt %(size)s um die Nachrichten vom Raum %(rooms)s zu speichern.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten von den Räumen %(rooms)s zu speichern.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten vom Raum %(rooms)s zu speichern.",
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur ihr zwei seid in dieser Konversation, außer einer von euch lädt jemanden neues ein.",
"This is the beginning of your direct message history with <displayName/>.": "Dies ist der Beginn deiner Direktnachrichtenhistorie mit <displayName/>.",
"Topic: %(topic)s (<a>edit</a>)": "Thema: %(topic)s (<a>ändern</a>)",
@ -2641,7 +2641,7 @@
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Bitte wirf einen Blick auf <existingIssuesLink>existierende Bugs auf Github</existingIssuesLink>. Keinen gefunden? <newIssueLink>Erstelle einen neuen</newIssueLink>.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIPP: Wenn du einen Bug meldest, füge bitte <debugLogsLink>Debug-Logs</debugLogsLink> hinzu um uns zu helfen das Problem zu finden.",
"Invite by email": "Via Email einladen",
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Beginne eine Konversation mit jemanden unter Benutzung des Namens, Email-Addresse oder Benutzername (siehe <userId/>).",
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Beginne eine Konversation mit jemanden unter Benutzung des Namens, der Email-Adresse oder der Matrix-ID (wie <userId/>).",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Lade jemanden unter Benutzung seines Namens, E-Mailaddresse oder Benutzername (siehe <userId/>) ein, oder <a>teile diesen Raum</a>.",
"Approve widget permissions": "Rechte für das Widget genehmigen",
"This widget would like to:": "Dieses Widget würde gerne:",
@ -2963,7 +2963,7 @@
"sends fireworks": "sendet Feuerwerk",
"Sends the given message with fireworks": "Sendet die gewählte Nachricht mit Feuerwerk",
"sends confetti": "sendet Konfetti",
"Sends the given message with confetti": "Sendet die gewählte Nachricht ohne Konfetti",
"Sends the given message with confetti": "Sendet die gewählte Nachricht mit Konfetti",
"Show chat effects": "Chat-Effekte anzeigen",
"Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message": "Stellt ┬──┬ ( ゜-゜ノ) einer Klartextnachricht voran",
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Stellt (╯°□°)╯︵ ┻━┻ einer Klartextnachricht voran",
@ -2979,7 +2979,7 @@
"Sends the given message with snowfall": "Sendet die gewählte Nachricht mit Schneeflocken",
"Transfer": "Übertragen",
"Failed to transfer call": "Anruf-Übertragung fehlgeschlagen",
"A call can only be transferred to a single user.": "Ein Anruf kann nur auf einen einzelnen Nutzer übertragen werden.",
"A call can only be transferred to a single user.": "Ein Anruf kann nur auf eine:n einzelne:n Nutzer:in übertragen werden.",
"Set up with a Security Key": "Mit einem Sicherheitsschlüssel einrichten",
"Use Security Key": "Sicherheitsschlüssel benutzen",
"Use Security Key or Phrase": "Sicherheitsschlüssel oder -phrase benutzen",
@ -3003,7 +3003,7 @@
"Workspace: <networkLink/>": "Arbeitsraum: <networkLink/>",
"Dial pad": "Wähltastatur",
"There was an error looking up the phone number": "Beim Suchen der Telefonnummer ist ein Fehler aufgetreten",
"Change which room, message, or user you're viewing": "Ändere welchen Raum, Nachricht oder Nutzer du siehst",
"Change which room, message, or user you're viewing": "Ändere welchen Raum, Nachricht oder Nutzer:in du siehst",
"Unable to look up phone number": "Telefonnummer kann nicht gesucht werden",
"This session has detected that your Security Phrase and key for Secure Messages have been removed.": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.",
"A new Security Phrase and key for Secure Messages have been detected.": "Eine neue Sicherheitsphrase und ein neuer Schlüssel für sichere Nachrichten wurden erkannt.",
@ -3049,5 +3049,15 @@
"Recently visited rooms": "Kürzlich besuchte Räume",
"Show line numbers in code blocks": "Zeilennummern in Code-Blöcken anzeigen",
"Expand code blocks by default": "Code-Blöcke standardmäßig erweitern",
"Try again": "Erneut versuchen"
"Try again": "Erneut versuchen",
"Upgrade to pro": "Hochstufen zu Pro",
"Minimize dialog": "Dialog minimieren",
"Maximize dialog": "Dialog maximieren",
"%(hostSignupBrand)s Setup": "%(hostSignupBrand)s-Einrichtung",
"You should know": "Du solltest wissen",
"Privacy Policy": "Datenschutz-Richtlinie",
"Cookie Policy": "Cookie-Richtlinie",
"Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Erfahren mehr in unserer <privacyPolicyLink />, <termsOfServiceLink /> und <cookiePolicyLink />.",
"Failed to connect to your homeserver. Please close this dialog and try again.": "Verbindung zum Homeserver fehlgeschlagen. Bitte schließe diesen Dialog and versuche es erneut.",
"Abort": "Abbrechen"
}

Some files were not shown because too many files have changed in this diff Show more