Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/a11y/composer-list-autocomplete

 Conflicts:
	src/autocomplete/AutocompleteProvider.tsx
	src/components/views/rooms/Autocomplete.tsx
This commit is contained in:
Michael Telatynski 2021-05-18 12:49:11 +01:00
commit e9c258a930
118 changed files with 3100 additions and 1003 deletions

View file

@ -1,3 +1,113 @@
Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0)
## Security notice
matrix-react-sdk 3.21.0 fixes a low severity issue (GHSA-8796-gc9j-63rv)
related to file upload. When uploading a file, the local file preview can lead
to execution of scripts embedded in the uploaded file, but only after several
user interactions to open the preview in a separate tab. This only impacts the
local user while in the process of uploading. It cannot be exploited remotely
or by other users. Thanks to [Muhammad Zaid Ghifari](https://github.com/MR-ZHEEV)
for responsibly disclosing this via Matrix's Security Disclosure Policy.
## All changes
* Upgrade to JS SDK 11.0.0
* [Release] Add missing space on beta feedback dialog
[\#6019](https://github.com/matrix-org/matrix-react-sdk/pull/6019)
* [Release] Add feedback mechanism for beta features, namely Spaces
[\#6013](https://github.com/matrix-org/matrix-react-sdk/pull/6013)
* Add feedback mechanism for beta features, namely Spaces
[\#6012](https://github.com/matrix-org/matrix-react-sdk/pull/6012)
Changes in [3.21.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0-rc.1) (2021-05-11)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0...v3.21.0-rc.1)
* Upgrade to JS SDK 11.0.0-rc.1
* Add disclaimer about subspaces being experimental in add existing dialog
[\#5978](https://github.com/matrix-org/matrix-react-sdk/pull/5978)
* Spaces Beta release
[\#5933](https://github.com/matrix-org/matrix-react-sdk/pull/5933)
* Improve permissions error when adding new server to room directory
[\#6009](https://github.com/matrix-org/matrix-react-sdk/pull/6009)
* Allow user to progress through space creation & setup using Enter
[\#6006](https://github.com/matrix-org/matrix-react-sdk/pull/6006)
* Upgrade sanitize types
[\#6008](https://github.com/matrix-org/matrix-react-sdk/pull/6008)
* Upgrade `cheerio` and resolve type errors
[\#6007](https://github.com/matrix-org/matrix-react-sdk/pull/6007)
* Add slash commands support to edit message composer
[\#5865](https://github.com/matrix-org/matrix-react-sdk/pull/5865)
* Fix the two todays problem
[\#5940](https://github.com/matrix-org/matrix-react-sdk/pull/5940)
* Switch the Home Space out for an All rooms space
[\#5969](https://github.com/matrix-org/matrix-react-sdk/pull/5969)
* Show device ID in UserInfo when there is no device name
[\#5985](https://github.com/matrix-org/matrix-react-sdk/pull/5985)
* Switch back to release version of `sanitize-html`
[\#6005](https://github.com/matrix-org/matrix-react-sdk/pull/6005)
* Bump hosted-git-info from 2.8.8 to 2.8.9
[\#5998](https://github.com/matrix-org/matrix-react-sdk/pull/5998)
* Don't use the event's metadata to calc the scale of an image
[\#5982](https://github.com/matrix-org/matrix-react-sdk/pull/5982)
* Adjust MIME type of upload confirmation if needed
[\#5981](https://github.com/matrix-org/matrix-react-sdk/pull/5981)
* Forbid redaction of encryption events
[\#5991](https://github.com/matrix-org/matrix-react-sdk/pull/5991)
* Fix voice message playback being squished up against send button
[\#5988](https://github.com/matrix-org/matrix-react-sdk/pull/5988)
* Improve style of notification badges on the space panel
[\#5983](https://github.com/matrix-org/matrix-react-sdk/pull/5983)
* Add dev dependency for parse5 typings
[\#5990](https://github.com/matrix-org/matrix-react-sdk/pull/5990)
* Iterate Spaces admin UX around room management
[\#5977](https://github.com/matrix-org/matrix-react-sdk/pull/5977)
* Guard all isSpaceRoom calls behind the labs flag
[\#5979](https://github.com/matrix-org/matrix-react-sdk/pull/5979)
* Bump lodash from 4.17.20 to 4.17.21
[\#5986](https://github.com/matrix-org/matrix-react-sdk/pull/5986)
* Bump lodash from 4.17.19 to 4.17.21 in /test/end-to-end-tests
[\#5987](https://github.com/matrix-org/matrix-react-sdk/pull/5987)
* Bump ua-parser-js from 0.7.23 to 0.7.28
[\#5984](https://github.com/matrix-org/matrix-react-sdk/pull/5984)
* Update visual style of plain files in the timeline
[\#5971](https://github.com/matrix-org/matrix-react-sdk/pull/5971)
* Support for multiple streams (not MSC3077)
[\#5833](https://github.com/matrix-org/matrix-react-sdk/pull/5833)
* Update space ordering behaviour to match updates in MSC
[\#5963](https://github.com/matrix-org/matrix-react-sdk/pull/5963)
* Improve performance of search all spaces and space switching
[\#5976](https://github.com/matrix-org/matrix-react-sdk/pull/5976)
* Update colours and sizing for voice messages
[\#5970](https://github.com/matrix-org/matrix-react-sdk/pull/5970)
* Update link to Android SDK
[\#5973](https://github.com/matrix-org/matrix-react-sdk/pull/5973)
* Add cleanup functions for image view
[\#5962](https://github.com/matrix-org/matrix-react-sdk/pull/5962)
* Add a note about sharing your IP in P2P calls
[\#5961](https://github.com/matrix-org/matrix-react-sdk/pull/5961)
* Only aggregate DM notifications on the Space Panel in the Home Space
[\#5968](https://github.com/matrix-org/matrix-react-sdk/pull/5968)
* Add retry mechanism and progress bar to add existing to space dialog
[\#5975](https://github.com/matrix-org/matrix-react-sdk/pull/5975)
* Warn on access token reveal
[\#5755](https://github.com/matrix-org/matrix-react-sdk/pull/5755)
* Fix newly joined room appearing under the wrong space
[\#5945](https://github.com/matrix-org/matrix-react-sdk/pull/5945)
* Early rendering for voice messages in the timeline
[\#5955](https://github.com/matrix-org/matrix-react-sdk/pull/5955)
* Calculate the real waveform in the Playback class for voice messages
[\#5956](https://github.com/matrix-org/matrix-react-sdk/pull/5956)
* Don't recurse on arrayFastResample
[\#5957](https://github.com/matrix-org/matrix-react-sdk/pull/5957)
* Support a dark theme for voice messages
[\#5958](https://github.com/matrix-org/matrix-react-sdk/pull/5958)
* Handle no/blocked microphones in voice messages
[\#5959](https://github.com/matrix-org/matrix-react-sdk/pull/5959)
Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10) Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0)

2
__mocks__/empty.js Normal file
View file

@ -0,0 +1,2 @@
// Yes, this is empty.
module.exports = {};

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.20.0", "version": "3.21.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -80,7 +80,7 @@
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.13", "matrix-widget-api": "^0.1.0-beta.14",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",
@ -186,7 +186,10 @@
], ],
"moduleNameMapper": { "moduleNameMapper": {
"\\.(gif|png|svg|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js", "\\.(gif|png|svg|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json" "\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js"
}, },
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"/node_modules/(?!matrix-js-sdk).+$" "/node_modules/(?!matrix-js-sdk).+$"

View file

@ -54,6 +54,7 @@
@import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_PulsedAvatar.scss"; @import "./views/avatars/_PulsedAvatar.scss";
@import "./views/avatars/_WidgetAvatar.scss"; @import "./views/avatars/_WidgetAvatar.scss";
@import "./views/beta/_BetaCard.scss";
@import "./views/context_menus/_CallContextMenu.scss"; @import "./views/context_menus/_CallContextMenu.scss";
@import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_IconizedContextMenu.scss";
@import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss";
@ -62,6 +63,7 @@
@import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_BetaFeedbackDialog.scss";
@import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@ -96,6 +98,7 @@
@import "./views/dialogs/_SpaceSettingsDialog.scss"; @import "./views/dialogs/_SpaceSettingsDialog.scss";
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UntrustedDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss"; @import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";
@ -237,6 +240,7 @@
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss";
@import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss";
@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss";
@import "./views/settings/tabs/user/_LabsUserSettingsTab.scss";
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss";
@import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss";
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";

View file

@ -56,6 +56,12 @@ limitations under the License.
.mx_GroupFilterPanel .mx_TagTile { .mx_GroupFilterPanel .mx_TagTile {
// opacity: 0.5; // opacity: 0.5;
position: relative; position: relative;
.mx_BetaDot {
position: absolute;
right: -13px;
top: -11px;
}
} }
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype { .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {

View file

@ -17,6 +17,11 @@ limitations under the License.
.mx_MyGroups { .mx_MyGroups {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.mx_BetaCard {
margin: 0 72px;
max-width: 760px;
}
} }
.mx_MyGroups .mx_RoomHeader_simpleHeader { .mx_MyGroups .mx_RoomHeader_simpleHeader {
@ -30,7 +35,7 @@ limitations under the License.
flex-wrap: wrap; flex-wrap: wrap;
} }
.mx_MyGroups > :not(.mx_RoomHeader) { .mx_MyGroups > :not(.mx_RoomHeader):not(.mx_BetaCard) {
max-width: 960px; max-width: 960px;
margin: 40px; margin: 40px;
} }

View file

@ -93,6 +93,10 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
&:not(.mx_SpaceRoomView_landing) .mx_SpaceFeedbackPrompt {
width: $SpaceRoomViewInnerWidth;
}
.mx_SpaceRoomView_buttons { .mx_SpaceRoomView_buttons {
display: block; display: block;
margin-top: 44px; margin-top: 44px;
@ -103,6 +107,10 @@ $SpaceRoomViewInnerWidth: 428px;
padding: 8px 22px; padding: 8px 22px;
margin-left: 16px; margin-left: 16px;
} }
input.mx_AccessibleButton {
border: none; // override default styles
}
} }
.mx_Field { .mx_Field {
@ -133,6 +141,44 @@ $SpaceRoomViewInnerWidth: 428px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color; box-shadow: 2px 15px 30px $dialog-shadow-color;
border-radius: 8px; border-radius: 8px;
position: relative;
// XXX remove this when spaces leaves Beta
.mx_BetaCard_betaPill {
position: absolute;
right: 24px;
top: 32px;
}
// XXX remove this when spaces leaves Beta
.mx_SpaceRoomView_preview_spaceBetaPrompt {
font-weight: $font-semi-bold;
font-size: $font-14px;
line-height: $font-24px;
color: $primary-fg-color;
margin-top: 24px;
position: relative;
padding-left: 24px;
.mx_AccessibleButton_kind_link {
display: inline;
padding: 0;
font-size: inherit;
line-height: inherit;
}
&::before {
content: "";
position: absolute;
height: $font-24px;
width: 20px;
left: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
background-color: $secondary-fg-color;
}
}
.mx_SpaceRoomView_preview_inviter { .mx_SpaceRoomView_preview_inviter {
display: flex; display: flex;
@ -293,10 +339,19 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SearchBox { .mx_SearchBox {
margin: 0 0 20px; margin: 0 0 20px;
} }
.mx_SpaceFeedbackPrompt {
margin-bottom: 16px;
// hide the HR as we have our own
& + hr {
display: none;
}
}
} }
.mx_SpaceRoomView_privateScope { .mx_SpaceRoomView_privateScope {
.mx_AccessibleButton { > .mx_AccessibleButton {
@mixin SpacePillButton; @mixin SpacePillButton;
} }
@ -310,6 +365,23 @@ $SpaceRoomViewInnerWidth: 428px;
} }
.mx_SpaceRoomView_inviteTeammates { .mx_SpaceRoomView_inviteTeammates {
// XXX remove this when spaces leaves Beta
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
padding: 58px 16px 16px;
position: relative;
border-radius: 8px;
background-color: $header-panel-bg-color;
max-width: $SpaceRoomViewInnerWidth;
margin: 20px 0 30px;
box-sizing: border-box;
.mx_BetaCard_betaPill {
position: absolute;
left: 16px;
top: 16px;
}
}
.mx_SpaceRoomView_inviteTeammates_buttons { .mx_SpaceRoomView_inviteTeammates_buttons {
color: $secondary-fg-color; color: $secondary-fg-color;
margin-top: 28px; margin-top: 28px;
@ -391,3 +463,66 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
} }
.mx_SpaceFeedbackPrompt {
margin-top: 18px;
margin-bottom: 12px;
> hr {
border: none;
border-top: 1px solid $input-border-color;
margin-bottom: 12px;
}
> div {
display: flex;
flex-direction: row;
font-size: $font-15px;
line-height: $font-24px;
> span {
color: $secondary-fg-color;
position: relative;
padding-left: 32px;
font-size: inherit;
line-height: inherit;
margin-right: auto;
&::before {
content: '';
position: absolute;
left: 0;
top: 2px;
height: 20px;
width: 20px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_AccessibleButton_kind_link {
color: $accent-color;
position: relative;
padding: 0 0 0 24px;
margin-left: 8px;
font-size: inherit;
line-height: inherit;
&::before {
content: '';
position: absolute;
left: 0;
height: 16px;
width: 16px;
background-color: $accent-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
mask-position: center;
}
}
}
}

View file

@ -0,0 +1,114 @@
/*
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_BetaCard {
margin-bottom: 20px;
padding: 24px;
background-color: $settings-profile-placeholder-bg-color;
border-radius: 8px;
display: flex;
box-sizing: border-box;
> div {
.mx_BetaCard_title {
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
color: $primary-fg-color;
margin: 4px 0 14px;
.mx_BetaCard_betaPill {
margin-left: 12px;
}
}
.mx_BetaCard_caption {
font-size: $font-15px;
line-height: $font-20px;
color: $secondary-fg-color;
margin-bottom: 20px;
}
.mx_AccessibleButton {
display: block;
margin: 12px 0;
padding: 7px 40px;
width: auto;
}
.mx_BetaCard_disclaimer {
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
margin-top: 20px;
}
}
> img {
margin: auto 0 auto 20px;
width: 300px;
object-fit: contain;
height: 100%;
}
}
.mx_BetaCard_betaPill {
background-color: $accent-color-alt;
padding: 4px 10px;
border-radius: 8px;
text-transform: uppercase;
font-size: 12px;
line-height: 15px;
color: #FFFFFF;
display: inline-block;
vertical-align: text-bottom;
&.mx_BetaCard_betaPill_clickable {
cursor: pointer;
}
}
$pulse-color: $accent-color-alt;
$dot-size: 12px;
.mx_BetaDot {
border-radius: 50%;
margin: 10px;
height: $dot-size;
width: $dot-size;
transform: scale(1);
background: rgba($pulse-color, 1);
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
animation: mx_Beta_bluePulse 2s infinite;
animation-iteration-count: 20;
}
@keyframes mx_Beta_bluePulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
}
}

View file

@ -54,7 +54,8 @@ limitations under the License.
display: flex; display: flex;
margin-top: 12px; margin-top: 12px;
.mx_BaseAvatar { // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
.mx_DecoratedRoomAvatar {
margin-right: 12px; margin-right: 12px;
} }
@ -75,10 +76,124 @@ limitations under the License.
} }
.mx_AddExistingToSpace_section_spaces { .mx_AddExistingToSpace_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;
} }
} }
.mx_AddExistingToSpace_section_experimental {
position: relative;
border-radius: 8px;
margin: 12px 0;
padding: 8px 8px 8px 42px;
background-color: $header-panel-bg-color;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
&::before {
content: '';
position: absolute;
left: 10px;
top: calc(50% - 8px); // vertical centering
height: 16px;
width: 16px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_AddExistingToSpace_footer {
display: flex;
margin-top: 20px;
> span {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
.mx_ProgressBar {
height: 8px;
width: 100%;
@mixin ProgressBarBorderRadius 8px;
}
.mx_AddExistingToSpace_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
}
> * {
vertical-align: middle;
}
}
.mx_AddExistingToSpace_error {
padding-left: 12px;
> img {
align-self: center;
}
.mx_AddExistingToSpace_errorHeading {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
color: $notice-primary-color;
}
.mx_AddExistingToSpace_errorCaption {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $primary-fg-color;
}
}
.mx_AccessibleButton {
display: inline-block;
align-self: center;
}
.mx_AccessibleButton_kind_primary {
padding: 8px 36px;
}
.mx_AddExistingToSpace_retryButton {
margin-left: 12px;
padding-left: 24px;
position: relative;
&::before {
content: '';
position: absolute;
background-color: $primary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
left: 0;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
}
}
} }
.mx_AddExistingToSpaceDialog { .mx_AddExistingToSpaceDialog {
@ -163,88 +278,4 @@ limitations under the License.
.mx_AddExistingToSpace { .mx_AddExistingToSpace {
display: contents; display: contents;
} }
.mx_AddExistingToSpaceDialog_footer {
display: flex;
margin-top: 20px;
> span {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
.mx_ProgressBar {
height: 8px;
width: 100%;
@mixin ProgressBarBorderRadius 8px;
}
.mx_AddExistingToSpaceDialog_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
}
> * {
vertical-align: middle;
}
}
.mx_AddExistingToSpaceDialog_error {
padding-left: 12px;
> img {
align-self: center;
}
.mx_AddExistingToSpaceDialog_errorHeading {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
color: $notice-primary-color;
}
.mx_AddExistingToSpaceDialog_errorCaption {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $primary-fg-color;
}
}
.mx_AccessibleButton {
display: inline-block;
align-self: center;
}
.mx_AccessibleButton_kind_primary {
padding: 8px 36px;
}
.mx_AddExistingToSpaceDialog_retryButton {
margin-left: 12px;
padding-left: 24px;
position: relative;
&::before {
content: '';
position: absolute;
background-color: $primary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
left: 0;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
}
}
} }

View file

@ -0,0 +1,30 @@
/*
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_BetaFeedbackDialog {
.mx_BetaFeedbackDialog_subheading {
color: $primary-fg-color;
font-size: $font-14px;
line-height: $font-20px;
margin-bottom: 24px;
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
line-height: inherit;
}
}

View file

@ -0,0 +1,26 @@
/*
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_UntrustedDeviceDialog {
.mx_Dialog_title {
display: flex;
align-items: center;
.mx_E2EIcon {
margin-left: 0;
}
}
}

View file

@ -85,6 +85,7 @@ limitations under the License.
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
mask-size: 18px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
background-color: $message-action-bar-fg-color; background-color: $message-action-bar-fg-color;

View file

@ -17,18 +17,56 @@ limitations under the License.
.mx_ReactionsRow { .mx_ReactionsRow {
margin: 6px 0; margin: 6px 0;
color: $primary-fg-color; color: $primary-fg-color;
.mx_ReactionsRow_addReactionButton {
position: relative;
display: inline-block;
visibility: hidden; // show on hover of the .mx_EventTile
width: 24px;
height: 24px;
vertical-align: middle;
margin-left: 4px;
&::before {
content: '';
position: absolute;
height: 100%;
width: 100%;
mask-size: 16px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $tertiary-fg-color;
mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg');
}
&.mx_ReactionsRow_addReactionButton_active {
visibility: visible; // keep showing whilst the context menu is shown
}
&:hover, &.mx_ReactionsRow_addReactionButton_active {
&::before {
background-color: $primary-fg-color;
}
}
}
}
.mx_EventTile:hover .mx_ReactionsRow_addReactionButton {
visibility: visible;
} }
.mx_ReactionsRow_showAll { .mx_ReactionsRow_showAll {
text-decoration: none; text-decoration: none;
font-size: $font-10px; font-size: $font-12px;
font-weight: 600; line-height: $font-20px;
margin-left: 6px; margin-left: 4px;
vertical-align: top; vertical-align: middle;
&:hover, &:link, &:visited {
&:link, color: $tertiary-fg-color;
&:visited { }
color: $accent-color;
&:hover {
color: $primary-fg-color;
} }
} }

View file

@ -16,14 +16,15 @@ limitations under the License.
.mx_ReactionsRowButton { .mx_ReactionsRowButton {
display: inline-flex; display: inline-flex;
line-height: $font-21px; line-height: $font-20px;
margin-right: 6px; margin-right: 6px;
padding: 0 6px; padding: 1px 6px;
border: 1px solid $reaction-row-button-border-color; border: 1px solid $reaction-row-button-border-color;
border-radius: 10px; border-radius: 10px;
background-color: $reaction-row-button-bg-color; background-color: $reaction-row-button-bg-color;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
vertical-align: middle;
&:hover { &:hover {
border-color: $reaction-row-button-hover-border-color; border-color: $reaction-row-button-hover-border-color;

View file

@ -98,7 +98,7 @@ limitations under the License.
position: relative; position: relative;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 32px; border-radius: 8px;
&::before { &::before {
content: ''; content: '';
@ -114,6 +114,11 @@ limitations under the License.
} }
} }
.mx_RoomSublist_auxButton:hover,
.mx_RoomSublist_menuButton:hover {
background: $roomlist-button-bg-color;
}
// Hide the menu button by default // Hide the menu button by default
.mx_RoomSublist_menuButton { .mx_RoomSublist_menuButton {
visibility: hidden; visibility: hidden;

View file

@ -0,0 +1,25 @@
/*
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_LabsUserSettingsTab {
.mx_SettingsTab_section {
margin-top: 32px;
.mx_SettingsFlag {
margin-right: 0; // remove right margin to align with beta cards
}
}
}

View file

@ -29,6 +29,7 @@ $spacePanelWidth: 71px;
width: 480px; width: 480px;
box-sizing: border-box; box-sizing: border-box;
background-color: $primary-bg-color; background-color: $primary-bg-color;
position: relative;
> div { > div {
> h2 { > h2 {
@ -44,6 +45,13 @@ $spacePanelWidth: 71px;
} }
} }
// XXX remove this when spaces leaves Beta
.mx_BetaCard_betaPill {
position: absolute;
top: 24px;
right: 24px;
}
.mx_SpaceCreateMenuType { .mx_SpaceCreateMenuType {
@mixin SpacePillButton; @mixin SpacePillButton;
} }
@ -59,7 +67,7 @@ $spacePanelWidth: 71px;
width: 28px; width: 28px;
height: 28px; height: 28px;
position: relative; position: relative;
background-color: $theme-button-bg-color; background-color: $roomlist-button-bg-color;
border-radius: 14px; border-radius: 14px;
margin-bottom: 12px; margin-bottom: 12px;
@ -70,7 +78,7 @@ $spacePanelWidth: 71px;
width: 28px; width: 28px;
top: 0; top: 0;
left: 0; left: 0;
background-color: $muted-fg-color; background-color: $tertiary-fg-color;
transform: rotate(90deg); transform: rotate(90deg);
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 2px 3px; mask-position: 2px 3px;

View file

@ -46,7 +46,7 @@ limitations under the License.
} }
.mx_Clock { .mx_Clock {
width: 42px; // we're not using a monospace font, so fake it width: $font-42px; // we're not using a monospace font, so fake it
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
padding-left: 8px; // isolate from recording circle / play control padding-left: 8px; // isolate from recording circle / play control
} }

BIN
res/img/betas/spaces.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

View file

@ -1,7 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1" fill="white"> <mask id="path-1-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM13.389 11.7659C13.6794 11.3162 14.2793 11.187 14.729 11.4774C15.1788 11.7677 15.308 12.3676 15.0176 12.8174C13.9565 14.461 12.1059 15.5526 9.99965 15.5526C7.89343 15.5526 6.04281 14.461 4.98167 12.8174C4.69133 12.3677 4.82053 11.7677 5.27025 11.4774C5.71997 11.187 6.31991 11.3162 6.61025 11.7659C7.32946 12.88 8.57908 13.6141 9.99965 13.6141C11.4202 13.6141 12.6698 12.88 13.389 11.7659ZM8 8C8 8.82843 7.44036 9.5 6.75 9.5C6.05964 9.5 5.5 8.82843 5.5 8C5.5 7.17157 6.05964 6.5 6.75 6.5C7.44036 6.5 8 7.17157 8 8ZM13.25 9.5C13.9404 9.5 14.5 8.82843 14.5 8C14.5 7.17157 13.9404 6.5 13.25 6.5C12.5596 6.5 12 7.17157 12 8C12 8.82843 12.5596 9.5 13.25 9.5Z"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM7.99989 7.5C7.99989 8.33 7.32989 9 6.49989 9C5.66989 9 4.99989 8.33 4.99989 7.5C4.99989 6.67 5.66989 6 6.49989 6C7.32989 6 7.99989 6.67 7.99989 7.5ZM14.9999 7.5C14.9999 8.33 14.3299 9 13.4999 9C12.6699 9 11.9999 8.33 11.9999 7.5C11.9999 6.67 12.6699 6 13.4999 6C14.3299 6 14.9999 6.67 14.9999 7.5ZM9.99989 15.5C12.3299 15.5 14.3099 14.04 15.1099 12H4.88989C5.68989 14.04 7.66989 15.5 9.99989 15.5Z"/>
</mask> </mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM13.389 11.7659C13.6794 11.3162 14.2793 11.187 14.729 11.4774C15.1788 11.7677 15.308 12.3676 15.0176 12.8174C13.9565 14.461 12.1059 15.5526 9.99965 15.5526C7.89343 15.5526 6.04281 14.461 4.98167 12.8174C4.69133 12.3677 4.82053 11.7677 5.27025 11.4774C5.71997 11.187 6.31991 11.3162 6.61025 11.7659C7.32946 12.88 8.57908 13.6141 9.99965 13.6141C11.4202 13.6141 12.6698 12.88 13.389 11.7659ZM8 8C8 8.82843 7.44036 9.5 6.75 9.5C6.05964 9.5 5.5 8.82843 5.5 8C5.5 7.17157 6.05964 6.5 6.75 6.5C7.44036 6.5 8 7.17157 8 8ZM13.25 9.5C13.9404 9.5 14.5 8.82843 14.5 8C14.5 7.17157 13.9404 6.5 13.25 6.5C12.5596 6.5 12 7.17157 12 8C12 8.82843 12.5596 9.5 13.25 9.5Z" fill="black"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM7.99989 7.5C7.99989 8.33 7.32989 9 6.49989 9C5.66989 9 4.99989 8.33 4.99989 7.5C4.99989 6.67 5.66989 6 6.49989 6C7.32989 6 7.99989 6.67 7.99989 7.5ZM14.9999 7.5C14.9999 8.33 14.3299 9 13.4999 9C12.6699 9 11.9999 8.33 11.9999 7.5C11.9999 6.67 12.6699 6 13.4999 6C14.3299 6 14.9999 6.67 14.9999 7.5ZM9.99989 15.5C12.3299 15.5 14.3099 14.04 15.1099 12H4.88989C5.68989 14.04 7.66989 15.5 9.99989 15.5Z" fill="#737D8C"/>
<path d="M14.1748 11.4164C14.0254 11.6478 14.0919 11.9565 14.3233 12.1059C14.5546 12.2553 14.8633 12.1888 15.0127 11.9574L14.1748 11.4164ZM15.148 11.7479C15.2974 11.5165 15.2309 11.2078 14.9995 11.0584C14.7681 10.909 14.4595 10.9755 14.3101 11.2069L15.148 11.7479ZM15.0127 11.9574L15.148 11.7479L14.3101 11.2069L14.1748 11.4164L15.0127 11.9574ZM14.729 11.4774L15.27 10.6394L15.27 10.6394L14.729 11.4774ZM13.389 11.7659L12.5511 11.225V11.225L13.389 11.7659ZM15.0176 12.8174L14.1797 12.2764V12.2764L15.0176 12.8174ZM4.98167 12.8174L5.8196 12.2764L5.8196 12.2764L4.98167 12.8174ZM5.27025 11.4774L4.72928 10.6394L4.72928 10.6394L5.27025 11.4774ZM6.61025 11.7659L5.77233 12.3069L6.61025 11.7659ZM19.0026 10C19.0026 14.972 14.972 19.0026 10 19.0026V20.9974C16.0737 20.9974 20.9974 16.0737 20.9974 10H19.0026ZM10 0.997378C14.972 0.997378 19.0026 5.02799 19.0026 10H20.9974C20.9974 3.92632 16.0737 -0.997378 10 -0.997378V0.997378ZM0.997378 10C0.997378 5.02799 5.02799 0.997378 10 0.997378V-0.997378C3.92632 -0.997378 -0.997378 3.92632 -0.997378 10H0.997378ZM10 19.0026C5.02799 19.0026 0.997378 14.972 0.997378 10H-0.997378C-0.997378 16.0737 3.92632 20.9974 10 20.9974V19.0026ZM15.27 10.6394C14.3575 10.0503 13.1402 10.3125 12.5511 11.225L14.227 12.3069C14.2259 12.3086 14.2229 12.312 14.2187 12.315C14.215 12.3174 14.2118 12.3186 14.2092 12.3192C14.2067 12.3197 14.2033 12.32 14.1989 12.3192C14.1939 12.3183 14.1898 12.3164 14.1881 12.3153L15.27 10.6394ZM15.8555 13.3583C16.4447 12.4458 16.1825 11.2286 15.27 10.6394L14.1881 12.3153C14.1863 12.3142 14.1829 12.3113 14.18 12.307C14.1775 12.3033 14.1764 12.3001 14.1758 12.2976C14.1753 12.2951 14.175 12.2917 14.1758 12.2873C14.1767 12.2822 14.1786 12.2781 14.1797 12.2764L15.8555 13.3583ZM9.99965 16.55C12.4589 16.55 14.6186 15.2742 15.8555 13.3583L14.1797 12.2764C13.2943 13.6478 11.7528 14.5552 9.99965 14.5552V16.55ZM4.14375 13.3583C5.38067 15.2742 7.54041 16.55 9.99965 16.55V14.5552C8.24645 14.5552 6.70495 13.6478 5.8196 12.2764L4.14375 13.3583ZM4.72928 10.6394C3.81679 11.2286 3.55464 12.4458 4.14375 13.3583L5.8196 12.2764C5.8207 12.2781 5.82261 12.2822 5.8235 12.2873C5.82426 12.2917 5.824 12.2951 5.82346 12.2976C5.82292 12.3001 5.82175 12.3033 5.81926 12.307C5.81635 12.3113 5.81294 12.3142 5.81122 12.3153L4.72928 10.6394ZM7.44818 11.225C6.85907 10.3125 5.64178 10.0503 4.72928 10.6394L5.81122 12.3153C5.8095 12.3164 5.80542 12.3183 5.80034 12.3192C5.79597 12.32 5.79256 12.3197 5.79004 12.3192C5.78752 12.3186 5.7843 12.3174 5.78064 12.315C5.77637 12.3121 5.77344 12.3086 5.77233 12.3069L7.44818 11.225ZM9.99965 12.6167C8.93153 12.6167 7.99128 12.0662 7.44818 11.225L5.77233 12.3069C6.66764 13.6937 8.22663 14.6115 9.99965 14.6115V12.6167ZM12.5511 11.225C12.008 12.0662 11.0678 12.6167 9.99965 12.6167V14.6115C11.7727 14.6115 13.3316 13.6937 14.227 12.3069L12.5511 11.225ZM6.75 10.4974C8.15374 10.4974 8.99738 9.20127 8.99738 8H7.00262C7.00262 8.19305 6.93691 8.33907 6.86768 8.42215C6.80022 8.50311 6.75428 8.50262 6.75 8.50262V10.4974ZM4.50262 8C4.50262 9.20127 5.34626 10.4974 6.75 10.4974V8.50262C6.74572 8.50262 6.69978 8.50311 6.63232 8.42215C6.56309 8.33907 6.49738 8.19305 6.49738 8H4.50262ZM6.75 5.50262C5.34626 5.50262 4.50262 6.79873 4.50262 8H6.49738C6.49738 7.80695 6.56309 7.66093 6.63232 7.57785C6.69978 7.49689 6.74572 7.49738 6.75 7.49738V5.50262ZM8.99738 8C8.99738 6.79873 8.15374 5.50262 6.75 5.50262V7.49738C6.75428 7.49738 6.80022 7.49689 6.86768 7.57785C6.93691 7.66093 7.00262 7.80695 7.00262 8H8.99738ZM13.5026 8C13.5026 8.19305 13.4369 8.33907 13.3677 8.42215C13.3002 8.50311 13.2543 8.50262 13.25 8.50262V10.4974C14.6537 10.4974 15.4974 9.20127 15.4974 8H13.5026ZM13.25 7.49738C13.2543 7.49738 13.3002 7.49689 13.3677 7.57785C13.4369 7.66093 13.5026 7.80695 13.5026 8H15.4974C15.4974 6.79873 14.6537 5.50262 13.25 5.50262V7.49738ZM12.9974 8C12.9974 7.80695 13.0631 7.66093 13.1323 7.57785C13.1998 7.49689 13.2457 7.49738 13.25 7.49738V5.50262C11.8463 5.50262 11.0026 6.79873 11.0026 8H12.9974ZM13.25 8.50262C13.2457 8.50262 13.1998 8.50311 13.1323 8.42215C13.0631 8.33907 12.9974 8.19305 12.9974 8H11.0026C11.0026 9.20127 11.8463 10.4974 13.25 10.4974V8.50262Z" fill="black" mask="url(#path-1-inside-1)"/> <path d="M15.1099 12L16.0384 12.3641L16.5724 11.0026H15.1099V12ZM4.88989 12V11.0026H3.42744L3.96136 12.3641L4.88989 12ZM19.0026 10C19.0026 14.972 14.972 19.0026 10 19.0026V20.9974C16.0737 20.9974 20.9974 16.0737 20.9974 10H19.0026ZM10 0.997378C14.972 0.997378 19.0026 5.02799 19.0026 10H20.9974C20.9974 3.92632 16.0737 -0.997378 10 -0.997378V0.997378ZM0.997378 10C0.997378 5.02799 5.02799 0.997378 10 0.997378V-0.997378C3.92632 -0.997378 -0.997378 3.92632 -0.997378 10H0.997378ZM10 19.0026C5.02799 19.0026 0.997378 14.972 0.997378 10H-0.997378C-0.997378 16.0737 3.92632 20.9974 10 20.9974V19.0026ZM6.49989 9.99738C7.88073 9.99738 8.99727 8.88084 8.99727 7.5H7.00251C7.00251 7.77916 6.77906 8.00262 6.49989 8.00262V9.99738ZM4.00251 7.5C4.00251 8.88084 5.11906 9.99738 6.49989 9.99738V8.00262C6.22073 8.00262 5.99727 7.77916 5.99727 7.5H4.00251ZM6.49989 5.00262C5.11906 5.00262 4.00251 6.11916 4.00251 7.5H5.99727C5.99727 7.22084 6.22073 6.99738 6.49989 6.99738V5.00262ZM8.99727 7.5C8.99727 6.11916 7.88073 5.00262 6.49989 5.00262V6.99738C6.77906 6.99738 7.00251 7.22084 7.00251 7.5H8.99727ZM13.4999 9.99738C14.8807 9.99738 15.9973 8.88084 15.9973 7.5H14.0025C14.0025 7.77916 13.7791 8.00262 13.4999 8.00262V9.99738ZM11.0025 7.5C11.0025 8.88084 12.1191 9.99738 13.4999 9.99738V8.00262C13.2207 8.00262 12.9973 7.77916 12.9973 7.5H11.0025ZM13.4999 5.00262C12.1191 5.00262 11.0025 6.11916 11.0025 7.5H12.9973C12.9973 7.22084 13.2207 6.99738 13.4999 6.99738V5.00262ZM15.9973 7.5C15.9973 6.11916 14.8807 5.00262 13.4999 5.00262V6.99738C13.7791 6.99738 14.0025 7.22084 14.0025 7.5H15.9973ZM14.1814 11.6359C13.5241 13.3119 11.9006 14.5026 9.99989 14.5026V16.4974C12.7592 16.4974 15.0957 14.7681 16.0384 12.3641L14.1814 11.6359ZM4.88989 12.9974H15.1099V11.0026H4.88989V12.9974ZM9.99989 14.5026C8.09919 14.5026 6.47569 13.3119 5.81842 11.6359L3.96136 12.3641C4.9041 14.7681 7.24059 16.4974 9.99989 16.4974V14.5026Z" fill="#737D8C" mask="url(#path-1-inside-1)"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -1,5 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="5" cy="5.75" rx="1.5" ry="1.75" fill="black"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M20 1C19.4477 1 19 1.44772 19 2V4H17C16.4477 4 16 4.44771 16 5C16 5.55228 16.4477 6 17 6H19V8C19 8.55228 19.4477 9 20 9C20.5523 9 21 8.55228 21 8V6H23C23.5523 6 24 5.55228 24 5C24 4.44772 23.5523 4 23 4H21V2C21 1.44772 20.5523 1 20 1ZM7 9.5C7 8.67 7.67 8 8.5 8C9.33 8 10 8.67 10 9.5C10 10.33 9.33 11 8.5 11C7.67 11 7 10.33 7 9.5ZM15.5 11C16.33 11 17 10.33 17 9.5C17 8.67 16.33 8 15.5 8C14.67 8 14 8.67 14 9.5C14 10.33 14.67 11 15.5 11ZM12 17.5C14.33 17.5 16.31 16.04 17.11 14H6.89001C7.69001 16.04 9.67001 17.5 12 17.5ZM4 12C4 7.58172 7.58172 4 12 4C12.6108 4 13.2045 4.06827 13.7742 4.1972C14.3129 4.3191 14.8484 3.98125 14.9703 3.44258C15.0922 2.90392 14.7543 2.36843 14.2156 2.24653C13.502 2.08504 12.7603 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 11.7878 21.9934 11.5771 21.9803 11.368C21.9459 10.8168 21.4711 10.3978 20.9199 10.4323C20.3687 10.4667 19.9498 10.9414 19.9842 11.4926C19.9947 11.6603 20 11.8295 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12Z" fill="#737D8C"/>
<ellipse cx="13" cy="5.75" rx="1.5" ry="1.75" fill="black"/>
<path d="M4.66558 10.6543C4.47466 10.2867 4.0219 10.1435 3.65431 10.3344C3.28672 10.5253 3.1435 10.9781 3.33442 11.3457L4.66558 10.6543ZM14.678 11.3206C14.8551 10.9462 14.6951 10.4991 14.3206 10.322C13.9462 10.1449 13.4991 10.3049 13.322 10.6794L14.678 11.3206ZM4 11C3.33442 11.3457 3.33458 11.346 3.33475 11.3463C3.33481 11.3464 3.33499 11.3468 3.33512 11.347C3.33537 11.3475 3.33565 11.3481 3.33596 11.3486C3.33657 11.3498 3.33729 11.3512 3.3381 11.3527C3.33972 11.3558 3.34175 11.3596 3.34417 11.3641C3.34901 11.3731 3.35546 11.3849 3.36353 11.3993C3.37966 11.4282 3.40229 11.4677 3.43154 11.5162C3.48998 11.6131 3.57517 11.7467 3.68808 11.9048C3.91323 12.2199 4.25254 12.6377 4.71468 13.056C5.64215 13.8956 7.08135 14.75 9.06977 14.75V13.25C7.54656 13.25 6.45087 12.6044 5.72136 11.944C5.35502 11.6123 5.08532 11.2801 4.90858 11.0327C4.82054 10.9095 4.75657 10.8088 4.71608 10.7416C4.69586 10.7081 4.68159 10.6831 4.67318 10.668C4.66898 10.6605 4.66625 10.6555 4.66499 10.6531C4.66435 10.652 4.66409 10.6515 4.66419 10.6516C4.66424 10.6517 4.66438 10.652 4.66461 10.6524C4.66473 10.6527 4.66487 10.6529 4.66503 10.6532C4.66511 10.6534 4.66525 10.6537 4.66529 10.6537C4.66543 10.654 4.66558 10.6543 4 11ZM9.06977 14.75C11.0611 14.75 12.4696 13.893 13.3669 13.0451C13.8129 12.6236 14.1351 12.2027 14.3471 11.8853C14.4535 11.7261 14.5331 11.5914 14.5875 11.4936C14.6148 11.4446 14.6358 11.4047 14.6508 11.3754C14.6583 11.3608 14.6643 11.3488 14.6688 11.3396C14.6711 11.335 14.673 11.3311 14.6745 11.3279C14.6753 11.3263 14.676 11.3249 14.6765 11.3237C14.6768 11.3231 14.6771 11.3225 14.6774 11.322C14.6775 11.3218 14.6776 11.3214 14.6777 11.3213C14.6779 11.3209 14.678 11.3206 14 11C13.322 10.6794 13.3221 10.6791 13.3223 10.6788C13.3223 10.6787 13.3224 10.6784 13.3225 10.6783C13.3227 10.6779 13.3228 10.6776 13.3229 10.6774C13.3232 10.6769 13.3233 10.6766 13.3234 10.6764C13.3235 10.6762 13.3233 10.6766 13.3228 10.6776C13.3217 10.6798 13.3193 10.6846 13.3156 10.6919C13.3081 10.7066 13.2952 10.7312 13.2768 10.7642C13.24 10.8304 13.1813 10.9301 13.0998 11.0522C12.9361 11.2973 12.6842 11.6264 12.3366 11.9549C11.6466 12.607 10.5901 13.25 9.06977 13.25V14.75Z" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -52,6 +52,9 @@ declare global {
init: () => Promise<void>; init: () => Promise<void>;
}; };
// Needed for Safari, unknown to TypeScript
webkitAudioContext: typeof AudioContext;
mxContentMessages: ContentMessages; mxContentMessages: ContentMessages;
mxToastStore: ToastStore; mxToastStore: ToastStore;
mxDeviceListener: DeviceListener; mxDeviceListener: DeviceListener;

View file

@ -40,6 +40,8 @@ export function eventTriggersUnreadCount(ev) {
return false; return false;
} else if (ev.getType() == 'm.room.server_acl') { } else if (ev.getType() == 'm.room.server_acl') {
return false; return false;
} else if (ev.isRedacted()) {
return false;
} }
return haveTileForEvent(ev); return haveTileForEvent(ev);
} }

View file

@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
const onKeyDownHandler = useCallback((ev) => { const onKeyDownHandler = useCallback((ev) => {
let handled = false; let handled = false;
// Don't interfere with input default keydown behaviour // Don't interfere with input default keydown behaviour
if (handleHomeEnd && ev.target.tagName !== "INPUT") { if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
// check if we actually have any items // check if we actually have any items
switch (ev.key) { switch (ev.key) {
case Key.HOME: case Key.HOME:

View file

@ -93,7 +93,14 @@ export default abstract class AutocompleteProvider {
}; };
} }
abstract getCompletions(query: string, selection: ISelectionRange, force: boolean): Promise<ICompletion[]>; abstract getCompletions(
query: string,
selection: ISelectionRange,
force: boolean,
limit: number,
): Promise<ICompletion[]> {
return [];
}
abstract getName(): string; abstract getName(): string;

View file

@ -26,6 +26,8 @@ import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider'; import NotifProvider from './NotifProvider';
import {timeout} from "../utils/promise"; import {timeout} from "../utils/promise";
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
import SettingsStore from "../settings/SettingsStore";
import SpaceProvider from "./SpaceProvider";
export interface ISelectionRange { export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not beginning?: boolean; // whether the selection is in the first block of the editor or not
@ -56,6 +58,11 @@ const PROVIDERS = [
DuckDuckGoProvider, DuckDuckGoProvider,
]; ];
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
if (SettingsStore.getValue("feature_spaces")) {
PROVIDERS.push(SpaceProvider);
}
// Providers will get rejected if they take longer than this. // Providers will get rejected if they take longer than this.
const PROVIDER_COMPLETION_TIMEOUT = 3000; const PROVIDER_COMPLETION_TIMEOUT = 3000;
@ -82,15 +89,24 @@ export default class Autocompleter {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<IProviderCompletions[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<IProviderCompletions[]> {
/* Note: This intentionally waits for all providers to return, /* Note: This intentionally waits for all providers to return,
otherwise, we run into a condition where new completions are displayed otherwise, we run into a condition where new completions are displayed
while the user is interacting with the list, which makes it difficult while the user is interacting with the list, which makes it difficult
to predict whether an action will actually do what is intended to predict whether an action will actually do what is intended
*/ */
// list of results from each provider, each being a list of completions or null if it times out // list of results from each provider, each being a list of completions or null if it times out
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(provider => { const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => {
return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT); return await timeout(
provider.getCompletions(query, selection, force, limit),
null,
PROVIDER_COMPLETION_TIMEOUT,
);
})); }));
// map then filter to maintain the index for the map-operation, for this.providers to line up // map then filter to maintain the index for the map-operation, for this.providers to line up

View file

@ -38,7 +38,12 @@ export default class CommandProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force?: boolean,
limit = -1,
): Promise<ICompletion[]> {
const {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (!command) return []; if (!command) return [];
@ -55,10 +60,11 @@ export default class CommandProvider extends AutocompleteProvider {
} else { } else {
if (query === '/') { if (query === '/') {
// If they have just entered `/` show everything // If they have just entered `/` show everything
// We exclude the limit on purpose to have a comprehensive list
matches = Commands; matches = Commands;
} else { } else {
// otherwise fuzzy match against all of the fields // otherwise fuzzy match against all of the fields
matches = this.matcher.match(command[1]); matches = this.matcher.match(command[1], limit);
} }
} }

View file

@ -50,7 +50,12 @@ export default class CommunityProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
// Disable autocompletions when composing commands because of various issues // Disable autocompletions when composing commands because of various issues
@ -81,7 +86,7 @@ export default class CommunityProvider extends AutocompleteProvider {
this.matcher.setObjects(groups); this.matcher.setObjects(groups);
const matchedString = command[0]; const matchedString = command[0];
completions = this.matcher.match(matchedString); completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [ completions = sortBy(completions, [
(c) => score(matchedString, c.groupId), (c) => score(matchedString, c.groupId),
(c) => c.groupId.length, (c) => c.groupId.length,

View file

@ -36,7 +36,12 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
} }
async getCompletions(query: string, selection: ISelectionRange, force= false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (!query || !command) { if (!query || !command) {
return []; return [];
@ -46,7 +51,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
method: 'GET', method: 'GET',
}); });
const json = await response.json(); const json = await response.json();
const results = json.Results.map((result) => { const maxLength = limit > -1 ? limit : json.Results.length;
const results = json.Results.slice(0, maxLength).map((result) => {
return { return {
completion: result.Text, completion: result.Text,
component: ( component: (

View file

@ -84,7 +84,12 @@ export default class EmojiProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force?: boolean,
limit = -1,
): Promise<ICompletion[]> {
if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) {
return []; // don't give any suggestions if the user doesn't want them return []; // don't give any suggestions if the user doesn't want them
} }
@ -93,7 +98,7 @@ export default class EmojiProvider extends AutocompleteProvider {
const {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (command) { if (command) {
const matchedString = command[0]; const matchedString = command[0];
completions = this.matcher.match(matchedString); completions = this.matcher.match(matchedString, limit);
// Do second match with shouldMatchWordsOnly in order to match against 'name' // Do second match with shouldMatchWordsOnly in order to match against 'name'
completions = completions.concat(this.nameMatcher.match(matchedString)); completions = completions.concat(this.nameMatcher.match(matchedString));

View file

@ -33,7 +33,12 @@ export default class NotifProvider extends AutocompleteProvider {
this.room = room; this.room = room;
} }
async getCompletions(query: string, selection: ISelectionRange, force= false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();

View file

@ -87,7 +87,7 @@ export default class QueryMatcher<T extends Object> {
} }
} }
match(query: string): T[] { match(query: string, limit = -1): T[] {
query = this.processQuery(query); query = this.processQuery(query);
if (this._options.shouldMatchWordsOnly) { if (this._options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, ''); query = query.replace(/[^\w]/g, '');
@ -129,7 +129,10 @@ export default class QueryMatcher<T extends Object> {
}); });
// Now map the keys to the result objects. Also remove any duplicates. // Now map the keys to the result objects. Also remove any duplicates.
return uniq(matches.map((match) => match.object)); const dedupped = uniq(matches.map((match) => match.object));
const maxLength = limit === -1 ? dedupped.length : limit;
return dedupped.slice(0, maxLength);
} }
private processQuery(query: string): string { private processQuery(query: string): string {

View file

@ -1,8 +1,7 @@
/* /*
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2017, 2018, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,17 +16,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import {uniqBy, sortBy} from "lodash";
import Room from "matrix-js-sdk/src/models/room"; import Room from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher'; import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components'; import {PillCompletion} from './Components';
import * as sdk from '../index';
import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter"; import {ICompletion, ISelectionRange} from "./Autocompleter";
import {uniqBy, sortBy} from "lodash"; import RoomAvatar from '../components/views/avatars/RoomAvatar';
import SettingsStore from "../settings/SettingsStore";
const ROOM_REGEX = /\B#\S*/g; const ROOM_REGEX = /\B#\S*/g;
@ -49,7 +50,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") {
} }
export default class RoomProvider extends AutocompleteProvider { export default class RoomProvider extends AutocompleteProvider {
matcher: QueryMatcher<Room>; protected matcher: QueryMatcher<Room>;
constructor() { constructor() {
super(ROOM_REGEX); super(ROOM_REGEX);
@ -58,15 +59,28 @@ export default class RoomProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> { protected getRooms() {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const cli = MatrixClientPeg.get();
let rooms = cli.getVisibleRooms();
const client = MatrixClientPeg.get(); if (SettingsStore.getValue("feature_spaces")) {
rooms = rooms.filter(r => !r.isSpaceRoom());
}
return rooms;
}
async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
let completions = []; let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force); const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) { if (command) {
// the only reason we need to do this is because Fuse only matches on properties // the only reason we need to do this is because Fuse only matches on properties
let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => { let matcherObjects = this.getRooms().reduce((aliases, room) => {
if (room.getCanonicalAlias()) { if (room.getCanonicalAlias()) {
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
} }
@ -90,7 +104,7 @@ export default class RoomProvider extends AutocompleteProvider {
this.matcher.setObjects(matcherObjects); this.matcher.setObjects(matcherObjects);
const matchedString = command[0]; const matchedString = command[0];
completions = this.matcher.match(matchedString); completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [ completions = sortBy(completions, [
(c) => score(matchedString, c.displayedAlias), (c) => score(matchedString, c.displayedAlias),
(c) => c.displayedAlias.length, (c) => c.displayedAlias.length,
@ -110,7 +124,7 @@ export default class RoomProvider extends AutocompleteProvider {
), ),
range, range,
}; };
}).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4); }).filter((completion) => !!completion.completion && completion.completion.length > 0);
} }
return completions; return completions;
} }

View file

@ -0,0 +1,43 @@
/*
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 { _t } from '../languageHandler';
import {MatrixClientPeg} from '../MatrixClientPeg';
import RoomProvider from "./RoomProvider";
export default class SpaceProvider extends RoomProvider {
protected getRooms() {
return MatrixClientPeg.get().getVisibleRooms().filter(r => r.isSpaceRoom());
}
getName() {
return _t("Spaces");
}
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
aria-label={_t("Space Autocomplete")}
>
{ completions }
</div>
);
}
}

View file

@ -102,7 +102,12 @@ export default class UserProvider extends AutocompleteProvider {
this.users = null; this.users = null;
}; };
async getCompletions(rawQuery: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> { async getCompletions(
rawQuery: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
// lazy-load user list into matcher // lazy-load user list into matcher
@ -118,7 +123,7 @@ export default class UserProvider extends AutocompleteProvider {
if (fullMatch && fullMatch !== '@') { if (fullMatch && fullMatch !== '@') {
// Don't include the '@' in our search query - it's only used as a way to trigger completion // Don't include the '@' in our search query - it's only used as a way to trigger completion
const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch; const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch;
completions = this.matcher.match(query).map((user) => { completions = this.matcher.match(query, limit).map((user) => {
const displayName = (user.name || user.userId || ''); const displayName = (user.name || user.userId || '');
return { return {
// Length of completion should equal length of text in decorator. draft-js // Length of completion should equal length of text in decorator. draft-js

View file

@ -222,10 +222,12 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
}; };
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
// don't let keyboard handling escape the context menu
ev.stopPropagation();
if (!this.props.managed) { if (!this.props.managed) {
if (ev.key === Key.ESCAPE) { if (ev.key === Key.ESCAPE) {
this.props.onFinished(); this.props.onFinished();
ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
return; return;
@ -258,7 +260,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
if (handled) { if (handled) {
// consume all other keys in context menu // consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
}; };

View file

@ -123,12 +123,19 @@ class GroupFilterPanel extends React.Component {
mx_GroupFilterPanel_items_selected: itemsSelected, mx_GroupFilterPanel_items_selected: itemsSelected,
}); });
let betaDot;
if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) {
betaDot = <div className="mx_BetaDot" />;
}
let createButton = ( let createButton = (
<ActionButton <ActionButton
tooltip tooltip
label={_t("Communities")} label={_t("Communities")}
action="toggle_my_groups" action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" /> className="mx_TagTile mx_TagTile_plus">
{ betaDot }
</ActionButton>
); );
if (SettingsStore.getValue("feature_communities_v2_prototypes")) { if (SettingsStore.getValue("feature_communities_v2_prototypes")) {

View file

@ -27,7 +27,7 @@ import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg'; import { IMatrixClientCreds } from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions'; import TagOrderActions from '../../actions/TagOrderActions';
@ -219,16 +219,6 @@ class LoggedInView extends React.Component<IProps, IState> {
}); });
}; };
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
//
// This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors.
shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get());
}
canResetTimelineInRoom = (roomId) => { canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) { if (!this._roomView.current) {
return true; return true;

View file

@ -740,6 +740,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreenAfterLogin(); this.showScreenAfterLogin();
break; break;
case 'toggle_my_groups': case 'toggle_my_groups':
// persist that the user has interacted with this, use it to dismiss the beta dot
localStorage.setItem("mx_seenSpacesBeta", "1");
// We just dispatch the page change rather than have to worry about // We just dispatch the page change rather than have to worry about
// what the logic is for each of these branches. // what the logic is for each of these branches.
if (this.state.page_type === PageTypes.MyGroups) { if (this.state.page_type === PageTypes.MyGroups) {
@ -906,6 +908,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let presentedId = roomInfo.room_alias || roomInfo.room_id; let presentedId = roomInfo.room_alias || roomInfo.room_id;
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
if (room) { if (room) {
// Not all timeline events are decrypted ahead of time anymore
// Only the critical ones for a typical UI are
// This will start the decryption process for all events when a
// user views a room
room.decryptAllEvents();
const theAlias = Rooms.getDisplayAliasForRoom(room); const theAlias = Rooms.getDisplayAliasForRoom(room);
if (theAlias) { if (theAlias) {
presentedId = theAlias; presentedId = theAlias;
@ -1684,6 +1691,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const type = screen === "start_sso" ? "sso" : "cas"; const type = screen === "start_sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
} else if (screen === 'groups') { } else if (screen === 'groups') {
if (SettingsStore.getValue("feature_spaces")) {
dis.dispatch({ action: "view_home_page" });
return;
}
dis.dispatch({ dis.dispatch({
action: 'view_my_groups', action: 'view_my_groups',
}); });
@ -1767,6 +1778,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
subAction: params.action, subAction: params.action,
}); });
} else if (screen.indexOf('group/') === 0) { } else if (screen.indexOf('group/') === 0) {
if (SettingsStore.getValue("feature_spaces")) {
dis.dispatch({ action: "view_home_page" });
return;
}
const groupId = screen.substring(6); const groupId = screen.substring(6);
// TODO: Check valid group ID // TODO: Check valid group ID

View file

@ -34,6 +34,7 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
import DMRoomMap from "../../utils/DMRoomMap"; import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro"; import NewRoomIntro from "../views/rooms/NewRoomIntro";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message']; const continuedTypes = ['m.sticker', 'm.room.message'];
@ -471,6 +472,10 @@ export default class MessagePanel extends React.Component {
return {nextEvent, nextTile}; return {nextEvent, nextTile};
} }
get _roomHasPendingEdit() {
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
}
_getEventTiles() { _getEventTiles() {
this.eventNodes = {}; this.eventNodes = {};
@ -559,6 +564,13 @@ export default class MessagePanel extends React.Component {
} }
} }
if (!this.props.editState && this._roomHasPendingEdit) {
defaultDispatcher.dispatch({
action: "edit_event",
event: this.props.room.findEventById(this._roomHasPendingEdit),
});
}
if (grouper) { if (grouper) {
ret.push(...grouper.getTiles()); ret.push(...grouper.getTiles());
} }
@ -574,7 +586,6 @@ export default class MessagePanel extends React.Component {
const isEditing = this.props.editState && const isEditing = this.props.editState &&
this.props.editState.getEvent().getId() === mxEv.getId(); this.props.editState.getEvent().getId() === mxEv.getId();
// local echoes have a fake date, which could even be yesterday. Treat them // local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators. // as 'today' for the date separators.
let ts1 = mxEv.getTs(); let ts1 = mxEv.getTs();

View file

@ -25,6 +25,7 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import BetaCard from "../views/beta/BetaCard";
@replaceableComponent("structures.MyGroups") @replaceableComponent("structures.MyGroups")
export default class MyGroups extends React.Component { export default class MyGroups extends React.Component {
@ -139,6 +140,7 @@ export default class MyGroups extends React.Component {
</div> </div>
</div>*/} </div>*/}
</div> </div>
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
<div className="mx_MyGroups_content"> <div className="mx_MyGroups_content">
{ contentHeader } { contentHeader }
{ content } { content }

View file

@ -1917,7 +1917,7 @@ export default class RoomView extends React.Component<IProps, IState> {
); );
} }
if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) { if (this.state.room?.isSpaceRoom()) {
return <SpaceRoomView return <SpaceRoomView
space={this.state.room} space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts} justCreatedOpts={this.props.justCreatedOpts}

View file

@ -41,6 +41,7 @@ import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle"; import {useStateToggle} from "../../hooks/useStateToggle";
import {getOrder} from "../../stores/SpaceStore"; import {getOrder} from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import {linkifyElement} from "../../HtmlUtils";
interface IHierarchyProps { interface IHierarchyProps {
space: Room; space: Room;
@ -172,7 +173,16 @@ const Tile: React.FC<ITileProps> = ({
{ suggestedSection } { suggestedSection }
</div> </div>
<div className="mx_SpaceRoomDirectory_roomTile_info"> <div
className="mx_SpaceRoomDirectory_roomTile_info"
ref={e => e && linkifyElement(e)}
onClick={ev => {
// prevent clicks on links from bubbling up to the room tile
if ((ev.target as HTMLElement).tagName === "A") {
ev.stopPropagation();
}
}}
>
{ description } { description }
</div> </div>
<div className="mx_SpaceRoomDirectory_actions"> <div className="mx_SpaceRoomDirectory_actions">
@ -574,7 +584,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
return <> return <>
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Search names and description") } placeholder={ _t("Search names and descriptions") }
onSearch={setQuery} onSearch={setQuery}
autoFocus={true} autoFocus={true}
initialValue={initialText} initialValue={initialText}

View file

@ -52,14 +52,19 @@ import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile"; import FacePile from "../views/elements/FacePile";
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
import {sleep} from "../../utils/promise";
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu"; import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
import IconizedContextMenu, { import IconizedContextMenu, {
IconizedContextMenuOption, IconizedContextMenuOption,
IconizedContextMenuOptionList, IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu"; } from "../views/context_menus/IconizedContextMenu";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import {BetaPill} from "../views/beta/BetaCard";
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
import SettingsStore from "../../settings/SettingsStore";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig";
interface IProps { interface IProps {
space: Room; space: Room;
@ -71,6 +76,7 @@ interface IProps {
interface IState { interface IState {
phase: Phase; phase: Phase;
createdRooms?: boolean; // internal state for the creation wizard
showRightPanel: boolean; showRightPanel: boolean;
myMembership: string; myMembership: string;
} }
@ -85,6 +91,26 @@ enum Phase {
PrivateExistingRooms, PrivateExistingRooms,
} }
// XXX: Temporary for the Spaces Beta only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<hr />
<div>
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
<AccessibleButton kind="link" onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
featureId: "feature_spaces",
});
}}>
{ _t("Feedback") }
</AccessibleButton>
</div>
</div>;
};
const RoomMemberCount = ({ room, children }) => { const RoomMemberCount = ({ room, children }) => {
const members = useRoomMembers(room); const members = useRoomMembers(room);
const count = members.length; const count = members.length;
@ -136,15 +162,39 @@ const SpaceInfo = ({ space }) => {
</div> </div>
}; };
const onBetaClick = () => {
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB,
});
};
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const spacesEnabled = SettingsStore.getValue("feature_spaces");
let inviterSection; let inviterSection;
let joinButtons; let joinButtons;
if (myMembership === "invite") { if (myMembership === "join") {
// XXX remove this when spaces leaves Beta
joinButtons = (
<AccessibleButton
kind="danger_outline"
onClick={() => {
dis.dispatch({
action: "leave_room",
room_id: space.roomId,
});
}}
>
{ _t("Leave") }
</AccessibleButton>
);
} else if (myMembership === "invite") {
const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender(); const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
const inviter = inviteSender && space.getMember(inviteSender); const inviter = inviteSender && space.getMember(inviteSender);
@ -180,6 +230,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
setBusy(true); setBusy(true);
onJoinButtonClicked(); onJoinButtonClicked();
}} }}
disabled={!spacesEnabled}
> >
{ _t("Accept") } { _t("Accept") }
</AccessibleButton> </AccessibleButton>
@ -192,10 +243,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
setBusy(true); setBusy(true);
onJoinButtonClicked(); onJoinButtonClicked();
}} }}
disabled={!spacesEnabled}
> >
{ _t("Join") } { _t("Join") }
</AccessibleButton> </AccessibleButton>
) );
} }
if (busy) { if (busy) {
@ -203,6 +255,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
} }
return <div className="mx_SpaceRoomView_preview"> return <div className="mx_SpaceRoomView_preview">
<BetaPill onClick={onBetaClick} />
{ inviterSection } { inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name"> <h1 className="mx_SpaceRoomView_preview_name">
@ -220,6 +273,20 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
<div className="mx_SpaceRoomView_preview_joinButtons"> <div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons } { joinButtons }
</div> </div>
{ !spacesEnabled && <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
{ myMembership === "join"
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
spaceName: space.name,
}, {
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
})
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
spaceName: space.name,
}, {
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
})
}
</div> }
</div>; </div>;
}; };
@ -353,6 +420,7 @@ const SpaceLanding = ({ space }) => {
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
<SpaceFeedbackPrompt />
<hr /> <hr />
<SpaceHierarchy <SpaceHierarchy
@ -382,14 +450,18 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
value={roomNames[i]} value={roomNames[i]}
onChange={ev => setRoomName(i, ev.target.value)} onChange={ev => setRoomName(i, ev.target.value)}
autoFocus={i === 2} autoFocus={i === 2}
disabled={busy}
/>; />;
}); });
const onNextClick = async () => { const onNextClick = async (ev) => {
ev.preventDefault();
if (busy) return;
setError(""); setError("");
setBusy(true); setBusy(true);
try { try {
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => { const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean);
await Promise.all(filteredRoomNames.map(name => {
return createRoom({ return createRoom({
createOpts: { createOpts: {
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat, preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
@ -402,7 +474,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
parentSpace: space, parentSpace: space,
}); });
})); }));
onFinished(); onFinished(filteredRoomNames.length > 0);
} catch (e) { } catch (e) {
console.error("Failed to create initial space rooms", e); console.error("Failed to create initial space rooms", e);
setError(_t("Failed to create initial space rooms")); setError(_t("Failed to create initial space rooms"));
@ -410,7 +482,10 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
setBusy(false); setBusy(false);
}; };
let onClick = onFinished; let onClick = (ev) => {
ev.preventDefault();
onFinished(false);
};
let buttonLabel = _t("Skip for now"); let buttonLabel = _t("Skip for now");
if (roomNames.some(name => name.trim())) { if (roomNames.some(name => name.trim())) {
onClick = onNextClick; onClick = onNextClick;
@ -422,54 +497,26 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
<div className="mx_SpaceRoomView_description">{ description }</div> <div className="mx_SpaceRoomView_description">{ description }</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> } { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
{ fields } <form onSubmit={onClick} id="mx_SpaceSetupFirstRooms">
{ fields }
</form>
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton <AccessibleButton
kind="primary" kind="primary"
disabled={busy} disabled={busy}
onClick={onClick} onClick={onClick}
> element="input"
{ buttonLabel } type="submit"
</AccessibleButton> form="mx_SpaceSetupFirstRooms"
value={buttonLabel}
/>
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
const SpaceAddExistingRooms = ({ space, onFinished }) => { const SpaceAddExistingRooms = ({ space, onFinished }) => {
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
let onClick = onFinished;
let buttonLabel = _t("Skip for now");
if (selectedToAdd.size > 0) {
onClick = async () => {
setBusy(true);
for (const room of selectedToAdd) {
const via = calculateRoomVia(room);
try {
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
if (e.errcode === "M_LIMIT_EXCEEDED") {
await sleep(e.data.retry_after_ms);
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
}
throw e;
});
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space"));
break;
}
}
setBusy(false);
};
buttonLabel = busy ? _t("Adding...") : _t("Add");
}
return <div> return <div>
<h1>{ _t("What do you want to organise?") }</h1> <h1>{ _t("What do you want to organise?") }</h1>
<div className="mx_SpaceRoomView_description"> <div className="mx_SpaceRoomView_description">
@ -477,36 +524,28 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
"no one will be informed. You can add more later.") } "no one will be informed. You can add more later.") }
</div> </div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} emptySelectionButton={
onChange={(checked, room) => { <AccessibleButton kind="primary" onClick={onFinished}>
if (checked) { { _t("Skip for now") }
selectedToAdd.add(room); </AccessibleButton>
} else { }
selectedToAdd.delete(room); onFinished={onFinished}
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/> />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton
kind="primary"
disabled={busy}
onClick={onClick}
>
{ buttonLabel }
</AccessibleButton>
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
const SpaceSetupPublicShare = ({ space, onFinished }) => { const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => {
return <div className="mx_SpaceRoomView_publicShare"> return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share %(name)s", { name: space.name }) }</h1> <h1>{ _t("Share %(name)s", {
name: justCreatedOpts?.createOpts?.name || space.name,
}) }</h1>
<div className="mx_SpaceRoomView_description"> <div className="mx_SpaceRoomView_description">
{ _t("It's just you at the moment, it will be even better with others.") } { _t("It's just you at the moment, it will be even better with others.") }
</div> </div>
@ -515,17 +554,20 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => {
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton kind="primary" onClick={onFinished}> <AccessibleButton kind="primary" onClick={onFinished}>
{ _t("Go to my first room") } { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
</AccessibleButton> </AccessibleButton>
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
const SpaceSetupPrivateScope = ({ space, onFinished }) => { const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
return <div className="mx_SpaceRoomView_privateScope"> return <div className="mx_SpaceRoomView_privateScope">
<h1>{ _t("Who are you working with?") }</h1> <h1>{ _t("Who are you working with?") }</h1>
<div className="mx_SpaceRoomView_description"> <div className="mx_SpaceRoomView_description">
{ _t("Make sure the right people have access to %(name)s", { name: space.name }) } { _t("Make sure the right people have access to %(name)s", {
name: justCreatedOpts?.createOpts?.name || space.name,
}) }
</div> </div>
<AccessibleButton <AccessibleButton
@ -542,6 +584,7 @@ const SpaceSetupPrivateScope = ({ space, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3> <h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div> <div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton> </AccessibleButton>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
@ -572,10 +615,13 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
ref={fieldRefs[i]} ref={fieldRefs[i]}
onValidate={validateEmailRules} onValidate={validateEmailRules}
autoFocus={i === 0} autoFocus={i === 0}
disabled={busy}
/>; />;
}); });
const onNextClick = async () => { const onNextClick = async (ev) => {
ev.preventDefault();
if (busy) return;
setError(""); setError("");
for (let i = 0; i < fieldRefs.length; i++) { for (let i = 0; i < fieldRefs.length; i++) {
const fieldRef = fieldRefs[i]; const fieldRef = fieldRefs[i];
@ -609,7 +655,10 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
setBusy(false); setBusy(false);
}; };
let onClick = onFinished; let onClick = (ev) => {
ev.preventDefault();
onFinished();
};
let buttonLabel = _t("Skip for now"); let buttonLabel = _t("Skip for now");
if (emailAddresses.some(name => name.trim())) { if (emailAddresses.some(name => name.trim())) {
onClick = onNextClick; onClick = onNextClick;
@ -622,8 +671,21 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
{ _t("Make sure the right people have access. You can invite more later.") } { _t("Make sure the right people have access. You can invite more later.") }
</div> </div>
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
<BetaPill onClick={onBetaClick} />
{ _t("<b>This is an experimental feature.</b> For now, " +
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
b: sub => <b>{ sub }</b>,
link: () => <a href="https://app.element.io/" rel="noreferrer noopener" target="_blank">
app.element.io
</a>,
}) }
</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> } { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
{ fields } <form onSubmit={onClick} id="mx_SpaceSetupPrivateInvite">
{ fields }
</form>
<div className="mx_SpaceRoomView_inviteTeammates_buttons"> <div className="mx_SpaceRoomView_inviteTeammates_buttons">
<AccessibleButton <AccessibleButton
@ -635,10 +697,17 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
</div> </div>
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton kind="primary" disabled={busy} onClick={onClick}> <AccessibleButton
{ buttonLabel } kind="primary"
</AccessibleButton> disabled={busy}
onClick={onClick}
element="input"
type="submit"
form="mx_SpaceSetupPrivateInvite"
value={buttonLabel}
/>
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
@ -759,7 +828,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() { private renderBody() {
switch (this.state.phase) { switch (this.state.phase) {
case Phase.Landing: case Phase.Landing:
if (this.state.myMembership === "join") { if (this.state.myMembership === "join" && SettingsStore.getValue("feature_spaces")) {
return <SpaceLanding space={this.props.space} />; return <SpaceLanding space={this.props.space} />;
} else { } else {
return <SpacePreview return <SpacePreview
@ -772,20 +841,26 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What are some things you want to discuss in %(spaceName)s?", { title={_t("What are some things you want to discuss in %(spaceName)s?", {
spaceName: this.props.space.name, spaceName: this.props.justCreatedOpts?.createOpts?.name || this.props.space.name,
})} })}
description={ description={
_t("Let's create a room for each of them.") + "\n" + _t("Let's create a room for each of them.") + "\n" +
_t("You can add more later too, including already existing ones.") _t("You can add more later too, including already existing ones.")
} }
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })}
/>; />;
case Phase.PublicShare: case Phase.PublicShare:
return <SpaceSetupPublicShare space={this.props.space} onFinished={this.goToFirstRoom} />; return <SpaceSetupPublicShare
justCreatedOpts={this.props.justCreatedOpts}
space={this.props.space}
onFinished={this.goToFirstRoom}
createdRooms={this.state.createdRooms}
/>;
case Phase.PrivateScope: case Phase.PrivateScope:
return <SpaceSetupPrivateScope return <SpaceSetupPrivateScope
space={this.props.space} space={this.props.space}
justCreatedOpts={this.props.justCreatedOpts}
onFinished={(invite: boolean) => { onFinished={(invite: boolean) => {
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms }); this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms });
}} }}
@ -801,7 +876,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
title={_t("What projects are you working on?")} title={_t("What projects are you working on?")}
description={_t("We'll create rooms for each of them. " + description={_t("We'll create rooms for each of them. " +
"You can add more later too, including already existing ones.")} "You can add more later too, including already existing ones.")}
onFinished={() => this.setState({ phase: Phase.Landing })} onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })}
/>; />;
case Phase.PrivateExistingRooms: case Phase.PrivateExistingRooms:
return <SpaceAddExistingRooms return <SpaceAddExistingRooms

View file

@ -38,6 +38,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature"; import {UIFeature} from "../../settings/UIFeature";
import {objectHasDiff} from "../../utils/objects"; import {objectHasDiff} from "../../utils/objects";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -1141,6 +1142,18 @@ class TimelinePanel extends React.Component {
// get the list of events from the timeline window and the pending event list // get the list of events from the timeline window and the pending event list
_getEvents() { _getEvents() {
const events = this._timelineWindow.getEvents(); const events = this._timelineWindow.getEvents();
// `arrayFastClone` performs a shallow copy of the array
// we want the last event to be decrypted first but displayed last
// `reverse` is destructive and unfortunately mutates the "events" array
arrayFastClone(events)
.reverse()
.forEach(event => {
if (event.shouldAttemptDecryption()) {
event.attemptDecryption(MatrixClientPeg.get()._crypto);
}
});
const firstVisibleEventIndex = this._checkForPreJoinUISI(events); const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
// Hold onto the live events separately. The read receipt and read marker // Hold onto the live events separately. The read receipt and read marker

View file

@ -179,7 +179,7 @@ const BaseAvatar = (props: IProps) => {
width: toPx(width), width: toPx(width),
height: toPx(height), height: toPx(height),
}} }}
title={title} alt="" title={title} alt={_t("Avatar")}
inputRef={inputRef} inputRef={inputRef}
{...otherProps} /> {...otherProps} />
); );

View file

@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user"; import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar"; import RoomAvatar from "./RoomAvatar";
import NotificationBadge from '../rooms/NotificationBadge'; import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
room: Room; room: Room;
avatarSize: number; avatarSize: number;
tag: TagID;
displayBadge?: boolean; displayBadge?: boolean;
forceCount?: boolean; forceCount?: boolean;
oobData?: object; oobData?: object;

View file

@ -0,0 +1,108 @@
/*
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 {_t} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
import TextWithTooltip from "../elements/TextWithTooltip";
import Modal from "../../../Modal";
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
interface IProps {
title?: string;
featureId: string;
}
export const BetaPill = ({ onClick }: { onClick?: () => void }) => {
if (onClick) {
return <TextWithTooltip
class={classNames("mx_BetaCard_betaPill", {
mx_BetaCard_betaPill_clickable: !!onClick,
})}
tooltip={<div>
<div className="mx_Tooltip_title">
{ _t("Spaces is a beta feature") }
</div>
<div className="mx_Tooltip_sub">
{ _t("Tap for more info") }
</div>
</div>}
onClick={onClick}
tooltipProps={{ yOffset: -10 }}
>
{ _t("Beta") }
</TextWithTooltip>;
}
return <span
className={classNames("mx_BetaCard_betaPill", {
mx_BetaCard_betaPill_clickable: !!onClick,
})}
onClick={onClick}
>
{ _t("Beta") }
</span>;
};
const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
const info = SettingsStore.getBetaInfo(featureId);
if (!info) return null; // Beta is invalid/disabled
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info;
const value = SettingsStore.getValue(featureId);
let feedbackButton;
if (value && feedbackLabel && feedbackSubheading && SdkConfig.get().bug_report_endpoint_url) {
feedbackButton = <AccessibleButton
onClick={() => {
Modal.createTrackedDialog("Beta Feedback", featureId, BetaFeedbackDialog, { featureId });
}}
kind="primary"
>
{ _t("Feedback") }
</AccessibleButton>;
}
return <div className="mx_BetaCard">
<div>
<h3 className="mx_BetaCard_title">
{ titleOverride || _t(title) }
<BetaPill />
</h3>
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
<div>
{ feedbackButton }
<AccessibleButton
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
kind={feedbackButton ? "primary_outline" : "primary"}
>
{ value ? _t("Leave the beta") : _t("Join the beta") }
</AccessibleButton>
</div>
{ disclaimer && <div className="mx_BetaCard_disclaimer">
{ disclaimer(value) }
</div> }
</div>
<img src={image} alt="" />
</div>;
};
export default BetaCard;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useContext, useMemo, useState} from "react"; import React, {ReactNode, useContext, useMemo, useState} from "react";
import classNames from "classnames"; import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
@ -36,6 +36,8 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar"; import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -45,7 +47,10 @@ interface IProps extends IDialogProps {
const Entry = ({ room, checked, onChange }) => { const Entry = ({ room, checked, onChange }) => {
return <label className="mx_AddExistingToSpace_entry"> return <label className="mx_AddExistingToSpace_entry">
<RoomAvatar room={room} height={32} width={32} /> { room?.isSpaceRoom()
? <RoomAvatar room={room} height={32} width={32} />
: <DecoratedRoomAvatar room={room} avatarSize={32} />
}
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span> <span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
<StyledCheckbox <StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null} onChange={onChange ? (e) => onChange(e.target.checked) : null}
@ -57,14 +62,23 @@ const Entry = ({ room, checked, onChange }) => {
interface IAddExistingToSpaceProps { interface IAddExistingToSpaceProps {
space: Room; space: Room;
selected: Set<Room>; footerPrompt?: ReactNode;
onChange(checked: boolean, room: Room): void; emptySelectionButton?: ReactNode;
onFinished(added: boolean): void;
} }
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => { export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
space,
footerPrompt,
emptySelectionButton,
onFinished,
}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase(); const lcQuery = query.toLowerCase();
@ -92,116 +106,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
return arr; return arr;
}, [[], [], []]); }, [[], [], []]);
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selected.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
let spaceOptionSection;
if (existingSubspaces.length > 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 rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
const addRooms = async () => { const addRooms = async () => {
setError(null); setError(null);
setProgress(0); setProgress(0);
@ -264,20 +168,145 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div> </div>
</span>; </span>;
} else { } else {
let button = emptySelectionButton;
if (!button || selectedToAdd.size > 0) {
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
{ _t("Add") }
</AccessibleButton>;
}
footer = <> footer = <>
<span> <span>
<div>{ _t("Want to add a new room instead?") }</div> { footerPrompt }
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</span> </span>
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}> { button }
{ _t("Add") }
</AccessibleButton>
</>; </>;
} }
const onChange = !busy && !error ? (checked, room) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
} : null;
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
<div className="mx_AddExistingToSpace_footer">
{ footer }
</div>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
let spaceOptionSection;
if (existingSubspaces.length > 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 rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
return <BaseDialog return <BaseDialog
title={title} title={title}
className="mx_AddExistingToSpaceDialog" className="mx_AddExistingToSpaceDialog"
@ -288,21 +317,17 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} onFinished={onFinished}
onChange={!busy && !error ? (checked, room) => { footerPrompt={<>
if (checked) { <div>{ _t("Want to add a new room instead?") }</div>
selectedToAdd.add(room); <AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
} else { { _t("Create a new room") }
selectedToAdd.delete(room); </AccessibleButton>
} </>}
setSelectedToAdd(new Set(selectedToAdd));
} : null}
/> />
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
<div className="mx_AddExistingToSpaceDialog_footer"> <SpaceFeedbackPrompt onClick={() => onFinished(false)} />
{ footer }
</div>
</BaseDialog>; </BaseDialog>;
}; };

View file

@ -0,0 +1,106 @@
/*
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 QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import {IDialogProps} from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import {submitFeedback} from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {USER_LABS_TAB} from "./UserSettingsDialog";
interface IProps extends IDialogProps {
featureId: string;
}
const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
const info = SettingsStore.getBetaInfo(featureId);
const [comment, setComment] = useState("");
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact);
onFinished(true);
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
title: _t("Beta feedback"),
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_BetaFeedbackDialog"
hasCancelButton={true}
title={_t("%(featureName)s beta feedback", { featureName: info.title })}
description={<React.Fragment>
<div className="mx_BetaFeedbackDialog_subheading">
{ _t(info.feedbackSubheading) }
&nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.")}
<AccessibleButton kind="link" onClick={() => {
onFinished(false);
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB,
});
}}>
{ _t("To leave the beta, visit your settings.") }
</AccessibleButton>
</div>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
autoFocus={true}
/>
<StyledCheckbox
checked={canContact}
onClick={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
};
export default BetaFeedbackDialog;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,7 +16,7 @@ limitations under the License.
import * as React from 'react'; import * as React from 'react';
import BaseDialog from './BaseDialog'; import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler'; import { _t, getUserLanguage } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { import {
ClientWidgetApi, ClientWidgetApi,
@ -39,6 +39,8 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import { arrayFastClone } from "../../../utils/arrays"; import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {ELEMENT_CLIENT_ID} from "../../../identifiers";
import SettingsStore from "../../../settings/SettingsStore";
interface IProps { interface IProps {
widgetDefinition: IModalWidgetOpenRequestData; widgetDefinition: IModalWidgetOpenRequestData;
@ -129,6 +131,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
currentUserId: MatrixClientPeg.get().getUserId(), currentUserId: MatrixClientPeg.get().getUserId(),
userDisplayName: OwnProfileStore.instance.displayName, userDisplayName: OwnProfileStore.instance.displayName,
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
clientId: ELEMENT_CLIENT_ID,
clientTheme: SettingsStore.getValue("theme"),
clientLanguage: getUserLanguage(),
}); });
const parsed = new URL(templated); const parsed = new URL(templated);

View file

@ -217,6 +217,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
value={this.state.otherHomeserver} value={this.state.otherHomeserver}
validateOnChange={false} validateOnChange={false}
validateOnFocus={false} validateOnFocus={false}
id="mx_homeserverInput"
/> />
</StyledRadioButton> </StyledRadioButton>
<p> <p>

View file

@ -32,6 +32,7 @@ import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import {allSettled} from "../../../utils/promise"; import {allSettled} from "../../../utils/promise";
import {useDispatcher} from "../../../hooks/useDispatcher"; import {useDispatcher} from "../../../hooks/useDispatcher";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -111,15 +112,17 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> } { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
<SpaceBasicSettings <SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")} avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
avatarDisabled={!canSetAvatar} avatarDisabled={busy || !canSetAvatar}
setAvatar={setNewAvatar} setAvatar={setNewAvatar}
name={name} name={name}
nameDisabled={!canSetName} nameDisabled={busy || !canSetName}
setName={setName} setName={setName}
topic={topic} topic={topic}
topicDisabled={!canSetTopic} topicDisabled={busy || !canSetTopic}
setTopic={setTopic} setTopic={setTopic}
/> />

View file

@ -0,0 +1,73 @@
/*
Copyright 2019, 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.
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 { User } from "matrix-js-sdk/src/models/user";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
import { IDevice } from "../right_panel/UserInfo";
interface IProps extends IDialogProps {
user: User;
device: IDevice;
}
const UntrustedDeviceDialog: React.FC<IProps> = ({device, user, onFinished}) => {
let askToVerifyText;
let newSessionText;
if (MatrixClientPeg.get().getUserId() === user.userId) {
newSessionText = _t("You signed in to a new session without verifying it:");
askToVerifyText = _t("Verify your other session using one of the options below.");
} else {
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
{name: user.displayName, userId: user.userId});
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
}
return <BaseDialog
onFinished={onFinished}
className="mx_UntrustedDeviceDialog"
title={<>
<E2EIcon status="warning" size={24} hideTooltip={true} />
{ _t("Not Trusted")}
</>}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{newSessionText}</p>
<p>{device.getDisplayName()} ({device.deviceId})</p>
<p>{askToVerifyText}</p>
</div>
<div className='mx_Dialog_buttons'>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>
{ _t("Manually Verify by Text") }
</AccessibleButton>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("sas")}>
{ _t("Interactively verify by Emoji") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={() => onFinished(false)}>
{ _t("Done") }
</AccessibleButton>
</div>
</BaseDialog>;
};
export default UntrustedDeviceDialog;

View file

@ -125,7 +125,10 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_securityIcon", "mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />, <SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
if (SdkConfig.get()['showLabsSettings']) { // Show the Labs tab if enabled or if there are any active betas
if (SdkConfig.get()['showLabsSettings']
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
) {
tabs.push(new Tab( tabs.push(new Tab(
USER_LABS_TAB, USER_LABS_TAB,
_td("Labs"), _td("Labs"),

View file

@ -345,6 +345,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}> <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
<input <input
type="password" type="password"
id="mx_passPhraseInput"
className="mx_AccessSecretStorageDialog_passPhraseInput" className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this.onPassPhraseChange} onChange={this.onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}

View file

@ -49,6 +49,18 @@ const inPlaceOf = (elementRect) => ({
}); });
const validServer = withValidation({ const validServer = withValidation({
deriveData: async ({ value }) => {
try {
// check if we can successfully load this server's room directory
await MatrixClientPeg.get().publicRooms({
limit: 1,
server: value,
});
return {};
} catch (error) {
return { error };
}
},
rules: [ rules: [
{ {
key: "required", key: "required",
@ -57,21 +69,11 @@ const validServer = withValidation({
}, { }, {
key: "available", key: "available",
final: true, final: true,
test: async ({ value }) => { test: async (_, { error }) => !error,
try {
const opts = {
limit: 1,
server: value,
};
// check if we can successfully load this server's room directory
await MatrixClientPeg.get().publicRooms(opts);
return true;
} catch (e) {
return false;
}
},
valid: () => _t("Looks good"), valid: () => _t("Looks good"),
invalid: () => _t("Can't find this server or its room list"), invalid: ({ error }) => error.errcode === "M_FORBIDDEN"
? _t("You are not allowed to view this server's rooms list")
: _t("Can't find this server or its room list"),
}, },
], ],
}); });

View file

@ -32,6 +32,7 @@ export default class ActionButton extends React.Component {
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
iconPath: PropTypes.string, iconPath: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node,
}; };
static defaultProps = { static defaultProps = {
@ -79,7 +80,8 @@ export default class ActionButton extends React.Component {
} }
return ( return (
<AccessibleButton className={classNames.join(" ")} <AccessibleButton
className={classNames.join(" ")}
onClick={this._onClick} onClick={this._onClick}
onMouseEnter={this._onMouseEnter} onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave} onMouseLeave={this._onMouseLeave}
@ -87,6 +89,7 @@ export default class ActionButton extends React.Component {
> >
{ icon } { icon }
{ tooltip } { tooltip }
{ this.props.children }
</AccessibleButton> </AccessibleButton>
); );
} }

View file

@ -37,7 +37,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
effect = new Effect(options); effect = new Effect(options);
effectsRef.current[name] = effect; effectsRef.current[name] = effect;
} catch (err) { } catch (err) {
console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err); console.warn(`Unable to load effect module at '../../../effects/${name}.`, err);
} }
} }
return effect; return effect;

View file

@ -207,6 +207,7 @@ export default class ImageView extends React.Component<IProps, IState> {
a.href = this.props.src; a.href = this.props.src;
a.download = this.props.name; a.download = this.props.name;
a.target = "_blank"; a.target = "_blank";
a.rel = "noreferrer noopener";
a.click(); a.click();
}; };
@ -442,16 +443,16 @@ export default class ImageView extends React.Component<IProps, IState> {
<div className="mx_ImageView_panel"> <div className="mx_ImageView_panel">
{info} {info}
<div className="mx_ImageView_toolbar"> <div className="mx_ImageView_toolbar">
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW" className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("Rotate Left")} title={_t("Rotate Left")}
onClick={ this.onRotateCounterClockwiseClick }> onClick={ this.onRotateCounterClockwiseClick }>
</AccessibleTooltipButton> </AccessibleTooltipButton>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
{zoomOutButton} {zoomOutButton}
{zoomInButton} {zoomInButton}
<AccessibleTooltipButton <AccessibleTooltipButton

View file

@ -58,13 +58,8 @@ export default class LanguageDropdown extends React.Component {
// If no value is given, we start with the first // If no value is given, we start with the first
// country selected, but our parent component // country selected, but our parent component
// doesn't know this, therefore we do this. // doesn't know this, therefore we do this.
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); const language = languageHandler.getUserLanguage();
if (language) { this.props.onOptionChange(language);
this.props.onOptionChange(language);
} else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language);
}
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,29 +14,72 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { isContentActionable } from '../../../utils/EventUtils'; import { isContentActionable } from '../../../utils/EventUtils';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu";
import ReactionPicker from "../emojipicker/ReactionPicker";
import ReactionsRowButton from "./ReactionsRowButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// The maximum number of reactions to initially show on a message. // The maximum number of reactions to initially show on a message.
const MAX_ITEMS_WHEN_LIMITED = 8; const MAX_ITEMS_WHEN_LIMITED = 8;
@replaceableComponent("views.messages.ReactionsRow") const ReactButton = ({ mxEvent, reactions }: IProps) => {
export default class ReactionsRow extends React.PureComponent { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
static propTypes = {
// The event we're displaying reactions for let contextMenu;
mxEvent: PropTypes.object.isRequired, if (menuDisplayed) {
// The Relations model from the JS SDK for reactions to `mxEvent` const buttonRect = button.current.getBoundingClientRect();
reactions: PropTypes.object, contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>;
} }
constructor(props) { return <React.Fragment>
super(props); <ContextMenuTooltipButton
className={classNames("mx_ReactionsRow_addReactionButton", {
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
})}
title={_t("Add reaction")}
onClick={openMenu}
onContextMenu={e => {
e.preventDefault();
openMenu();
}}
isExpanded={menuDisplayed}
inputRef={button}
/>
{ contextMenu }
</React.Fragment>;
};
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations;
}
interface IState {
myReactions: MatrixEvent[];
showAll: boolean;
}
@replaceableComponent("views.messages.ReactionsRow")
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
static contextType = MatrixClientContext;
constructor(props, context) {
super(props, context);
if (props.reactions) { if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange); props.reactions.on("Relations.add", this.onReactionsChange);
@ -92,7 +135,7 @@ export default class ReactionsRow extends React.PureComponent {
if (!reactions) { if (!reactions) {
return null; return null;
} }
const userId = MatrixClientPeg.get().getUserId(); const userId = this.context.getUserId();
const myReactions = reactions.getAnnotationsBySender()[userId]; const myReactions = reactions.getAnnotationsBySender()[userId];
if (!myReactions) { if (!myReactions) {
return null; return null;
@ -114,7 +157,6 @@ export default class ReactionsRow extends React.PureComponent {
return null; return null;
} }
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => { let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
const count = events.size; const count = events.size;
if (!count) { if (!count) {
@ -136,6 +178,8 @@ export default class ReactionsRow extends React.PureComponent {
/>; />;
}).filter(item => !!item); }).filter(item => !!item);
if (!items.length) return null;
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
// The "+ 1" ensure that the "show all" reveals something that takes up // The "+ 1" ensure that the "show all" reveals something that takes up
// more space than the button itself. // more space than the button itself.
@ -151,13 +195,21 @@ export default class ReactionsRow extends React.PureComponent {
</a>; </a>;
} }
const cli = this.context;
let addReactionButton;
if (cli.getRoom(mxEvent.getRoomId()).currentState.maySendEvent(EventType.Reaction, cli.getUserId())) {
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
}
return <div return <div
className="mx_ReactionsRow" className="mx_ReactionsRow"
role="toolbar" role="toolbar"
aria-label={_t("Reactions")} aria-label={_t("Reactions")}
> >
{items} { items }
{showAllButton} { showAllButton }
{ addReactionButton }
</div>; </div>;
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,49 +14,54 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import classNames from "classnames";
import classNames from 'classnames'; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The reaction content / key / emoji
content: string;
// The count of votes for this key
count: number;
// A Set of Matrix reaction events for this key
reactionEvents: Set<MatrixEvent>;
// A possible Matrix event if the current user has voted for this type
myReactionEvent?: MatrixEvent;
}
interface IState {
tooltipRendered: boolean;
tooltipVisible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButton") @replaceableComponent("views.messages.ReactionsRowButton")
export default class ReactionsRowButton extends React.PureComponent { export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
static propTypes = { static contextType = MatrixClientContext;
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The reaction content / key / emoji
content: PropTypes.string.isRequired,
// The count of votes for this key
count: PropTypes.number.isRequired,
// A Set of Martix reaction events for this key
reactionEvents: PropTypes.object.isRequired,
// A possible Matrix event if the current user has voted for this type
myReactionEvent: PropTypes.object,
}
constructor(props) { state = {
super(props); tooltipRendered: false,
tooltipVisible: false,
};
this.state = { onClick = () => {
tooltipVisible: false,
};
}
onClick = (ev) => {
const { mxEvent, myReactionEvent, content } = this.props; const { mxEvent, myReactionEvent, content } = this.props;
if (myReactionEvent) { if (myReactionEvent) {
MatrixClientPeg.get().redactEvent( this.context.redactEvent(
mxEvent.getRoomId(), mxEvent.getRoomId(),
myReactionEvent.getId(), myReactionEvent.getId(),
); );
} else { } else {
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", { this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
"m.relates_to": { "m.relates_to": {
"rel_type": "m.annotation", "rel_type": "m.annotation",
"event_id": mxEvent.getId(), "event_id": mxEvent.getId(),
@ -83,8 +88,6 @@ export default class ReactionsRowButton extends React.PureComponent {
} }
render() { render() {
const ReactionsRowButtonTooltip =
sdk.getComponent('messages.ReactionsRowButtonTooltip');
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props; const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
const classes = classNames({ const classes = classNames({
@ -102,7 +105,7 @@ export default class ReactionsRowButton extends React.PureComponent {
/>; />;
} }
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const room = this.context.getRoom(mxEvent.getRoomId());
let label; let label;
if (room) { if (room) {
const senders = []; const senders = [];
@ -130,7 +133,6 @@ export default class ReactionsRowButton extends React.PureComponent {
); );
} }
const isPeeking = room.getMyMembership() !== "join"; const isPeeking = room.getMyMembership() !== "join";
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <AccessibleButton return <AccessibleButton
className={classes} className={classes}
aria-label={label} aria-label={label}

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,33 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { unicodeToShortcode } from '../../../HtmlUtils'; import { unicodeToShortcode } from '../../../HtmlUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The reaction content / key / emoji
content: string;
// A Set of Matrix reaction events for this key
reactionEvents: Set<MatrixEvent>;
visible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButtonTooltip") @replaceableComponent("views.messages.ReactionsRowButtonTooltip")
export default class ReactionsRowButtonTooltip extends React.PureComponent { export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
static propTypes = { static contextType = MatrixClientContext;
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The reaction content / key / emoji
content: PropTypes.string.isRequired,
// A Set of Martix reaction events for this key
reactionEvents: PropTypes.object.isRequired,
visible: PropTypes.bool.isRequired,
}
render() { render() {
const Tooltip = sdk.getComponent('elements.Tooltip');
const { content, reactionEvents, mxEvent, visible } = this.props; const { content, reactionEvents, mxEvent, visible } = this.props;
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const room = this.context.getRoom(mxEvent.getRoomId());
let tooltipLabel; let tooltipLabel;
if (room) { if (room) {
const senders = []; const senders = [];

View file

@ -67,7 +67,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import {mediaFromMxc} from "../../../customisations/Media"; import {mediaFromMxc} from "../../../customisations/Media";
interface IDevice { export interface IDevice {
deviceId: string; deviceId: string;
ambiguous?: boolean; ambiguous?: boolean;
getDisplayName(): string; getDisplayName(): string;

View file

@ -25,6 +25,8 @@ import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter'; import Autocompleter from '../../../autocomplete/Autocompleter';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
const MAX_PROVIDER_MATCHES = 20;
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`; export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
interface IProps { interface IProps {
@ -134,7 +136,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
processQuery(query: string, selection: ISelectionRange) { processQuery(query: string, selection: ISelectionRange) {
return this.autocompleter.getCompletions( return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete, query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
).then((completions) => { ).then((completions) => {
// Only ever process the completions for the most recent query being processed // Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) { if (query !== this.queryRequested) {

View file

@ -35,6 +35,7 @@ import {Action} from "../../../dispatcher/actions";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import SendHistoryManager from '../../../SendHistoryManager';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
function _isReply(mxEvent) { function _isReply(mxEvent) {
@ -122,6 +123,7 @@ export default class EditMessageComposer extends React.Component {
saveDisabled: true, saveDisabled: true,
}; };
this._createEditorModel(); this._createEditorModel();
window.addEventListener("beforeunload", this._saveStoredEditorState);
} }
_setEditorRef = ref => { _setEditorRef = ref => {
@ -175,11 +177,55 @@ export default class EditMessageComposer extends React.Component {
} }
} }
get _editorRoomKey() {
return `mx_edit_room_${this._getRoom().roomId}`;
}
get _editorStateKey() {
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
}
_cancelEdit = () => { _cancelEdit = () => {
this._clearStoredEditorState();
dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: "edit_event", event: null});
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
} }
get _shouldSaveStoredEditorState() {
return localStorage.getItem(this._editorRoomKey) !== null;
}
_restoreStoredEditorState(partCreator) {
const json = localStorage.getItem(this._editorStateKey);
if (json) {
try {
const {parts: serializedParts} = JSON.parse(json);
const parts = serializedParts.map(p => partCreator.deserializePart(p));
return parts;
} catch (e) {
console.error("Error parsing editing state: ", e);
}
}
}
_clearStoredEditorState() {
localStorage.removeItem(this._editorRoomKey);
localStorage.removeItem(this._editorStateKey);
}
_clearPreviousEdit() {
if (localStorage.getItem(this._editorRoomKey)) {
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`);
}
}
_saveStoredEditorState() {
const item = SendHistoryManager.createItem(this.model);
this._clearPreviousEdit();
localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId());
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
}
_isSlashCommand() { _isSlashCommand() {
const parts = this.model.parts; const parts = this.model.parts;
const firstPart = parts[0]; const firstPart = parts[0];
@ -266,6 +312,7 @@ export default class EditMessageComposer extends React.Component {
const editedEvent = this.props.editState.getEvent(); const editedEvent = this.props.editState.getEvent();
const editContent = createEditContent(this.model, editedEvent); const editContent = createEditContent(this.model, editedEvent);
const newContent = editContent["m.new_content"]; const newContent = editContent["m.new_content"];
let shouldSend = true; let shouldSend = true;
// If content is modified then send an updated event into the room // If content is modified then send an updated event into the room
@ -311,6 +358,7 @@ export default class EditMessageComposer extends React.Component {
if (shouldSend) { if (shouldSend) {
this._cancelPreviousPendingEdit(); this._cancelPreviousPendingEdit();
const prom = this.context.sendMessage(roomId, editContent); const prom = this.context.sendMessage(roomId, editContent);
this._clearStoredEditorState();
dis.dispatch({action: "message_sent"}); dis.dispatch({action: "message_sent"});
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
} }
@ -346,6 +394,10 @@ export default class EditMessageComposer extends React.Component {
// then when mounting the editor again with the same editor state, // then when mounting the editor again with the same editor state,
// it will set the cursor at the end. // it will set the cursor at the end.
this.props.editState.setEditorState(caret, parts); this.props.editState.setEditorState(caret, parts);
window.removeEventListener("beforeunload", this._saveStoredEditorState);
if (this._shouldSaveStoredEditorState) {
this._saveStoredEditorState();
}
} }
_createEditorModel() { _createEditorModel() {
@ -358,10 +410,11 @@ export default class EditMessageComposer extends React.Component {
// restore serialized parts from the state // restore serialized parts from the state
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
} else { } else {
// otherwise, parse the body of the event //otherwise, either restore serialized parts from localStorage or parse the body of the event
parts = parseEvent(editState.getEvent(), partCreator); parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator);
} }
this.model = new EditorModel(parts, partCreator); this.model = new EditorModel(parts, partCreator);
this._saveStoredEditorState();
} }
_getInitialCaretPosition() { _getInitialCaretPosition() {

View file

@ -23,11 +23,9 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics"; import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition } from "react-transition-group"; import { CSSTransition } from "react-transition-group";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Toolbar from "../../../accessibility/Toolbar"; import Toolbar from "../../../accessibility/Toolbar";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
} }
@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
public render(): React.ReactElement { public render(): React.ReactElement {
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return ( return (
<RovingAccessibleTooltipButton <RovingAccessibleTooltipButton
className="mx_RoomBreadcrumbs_crumb" className="mx_RoomBreadcrumbs_crumb"
@ -98,7 +94,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
<DecoratedRoomAvatar <DecoratedRoomAvatar
room={r} room={r}
avatarSize={32} avatarSize={32}
tag={roomTag}
displayBadge={true} displayBadge={true}
forceCount={true} forceCount={true}
/> />

View file

@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic"; import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
@ -177,7 +176,6 @@ export default class RoomHeader extends React.Component {
roomAvatar = <DecoratedRoomAvatar roomAvatar = <DecoratedRoomAvatar
room={this.props.room} room={this.props.room}
avatarSize={32} avatarSize={32}
tag={DefaultTagID.Untagged} // to apply room publicity badging
oobData={this.props.oobData} oobData={this.props.oobData}
viewAvatarOnClick={true} viewAvatarOnClick={true}
/>; />;

View file

@ -576,7 +576,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const roomAvatar = <DecoratedRoomAvatar const roomAvatar = <DecoratedRoomAvatar
room={this.props.room} room={this.props.room}
avatarSize={32} avatarSize={32}
tag={this.props.tag}
displayBadge={this.props.isMinimized} displayBadge={this.props.isMinimized}
oobData={({avatarUrl: roomProfile.avatarMxc})} oobData={({avatarUrl: roomProfile.avatarMxc})}
/>; />;

View file

@ -232,7 +232,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
<p> <p>
{this.state.enabling {this.state.enabling
? <InlineSpinner /> ? <InlineSpinner />
: _t("Message search initilisation failed") : _t("Message search initialisation failed")
} }
</p> </p>
{EventIndexPeg.error && ( {EventIndexPeg.error && (

View file

@ -22,6 +22,8 @@ import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import * as sdk from "../../../../../index"; import * as sdk from "../../../../../index";
import {SettingLevel} from "../../../../../settings/SettingLevel"; import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import SdkConfig from "../../../../../SdkConfig";
import BetaCard from "../../../beta/BetaCard";
export class LabsSettingToggle extends React.Component { export class LabsSettingToggle extends React.Component {
static propTypes = { static propTypes = {
@ -48,14 +50,40 @@ export default class LabsUserSettingsTab extends React.Component {
} }
render() { render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const features = SettingsStore.getFeatureSettingNames();
const flags = SettingsStore.getFeatureSettingNames().map(f => <LabsSettingToggle featureId={f} key={f} />); const [labs, betas] = features.reduce((arr, f) => {
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
return arr;
}, [[], []]);
let betaSection;
if (betas.length) {
betaSection = <div className="mx_SettingsTab_section">
{ betas.map(f => <BetaCard key={f} featureId={f} /> ) }
</div>;
}
let labsSection;
if (SdkConfig.get()['showLabsSettings']) {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
labsSection = <div className="mx_SettingsTab_section">
{flags}
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
</div>;
}
return ( return (
<div className="mx_SettingsTab"> <div className="mx_SettingsTab mx_LabsUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Labs")}</div> <div className="mx_SettingsTab_heading">{_t("Labs")}</div>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{ {
_t('Customise your experience with experimental labs features. ' + _t('Feeling experimental? Labs are the best way to get things early, ' +
'test out new features and help shape them before they actually launch. ' +
'<a>Learn more</a>.', {}, { '<a>Learn more</a>.', {}, {
'a': (sub) => { 'a': (sub) => {
return <a href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md" return <a href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
@ -64,13 +92,8 @@ export default class LabsUserSettingsTab extends React.Component {
}) })
} }
</div> </div>
<div className="mx_SettingsTab_section"> { betaSection }
{flags} { labsSection }
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"advancedRoomListLogging"} level={SettingLevel.DEVICE} />
</div>
</div> </div>
); );
} }

View file

@ -32,17 +32,11 @@ interface IProps {
setTopic(topic: string): void; setTopic(topic: string): void;
} }
const SpaceBasicSettings = ({ export const SpaceAvatar = ({
avatarUrl, avatarUrl,
avatarDisabled = false, avatarDisabled = false,
setAvatar, setAvatar,
name = "", }: Pick<IProps, "avatarUrl" | "avatarDisabled" | "setAvatar">) => {
nameDisabled = false,
setName,
topic = "",
topicDisabled = false,
setTopic,
}: IProps) => {
const avatarUploadRef = useRef<HTMLInputElement>(); const avatarUploadRef = useRef<HTMLInputElement>();
const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
@ -81,20 +75,34 @@ const SpaceBasicSettings = ({
} }
} }
return <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>;
};
const SpaceBasicSettings = ({
avatarUrl,
avatarDisabled = false,
setAvatar,
name = "",
nameDisabled = false,
setName,
topic = "",
topicDisabled = false,
setTopic,
}: IProps) => {
return <div className="mx_SpaceBasicSettings"> return <div className="mx_SpaceBasicSettings">
<div className="mx_SpaceBasicSettings_avatarContainer"> <SpaceAvatar avatarUrl={avatarUrl} avatarDisabled={avatarDisabled} setAvatar={setAvatar} />
{ 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 <Field
name="spaceName" name="spaceName"

View file

@ -14,18 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useContext, useState} from "react"; import React, {useContext, useRef, useState} from "react";
import classNames from "classnames"; import classNames from "classnames";
import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event"; import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
import FocusLock from "react-focus-lock";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu"; import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
import createRoom, {IStateEvent, Preset} from "../../../createRoom"; import createRoom, {IStateEvent, Preset} from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import SpaceBasicSettings from "./SpaceBasicSettings"; import {SpaceAvatar} from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import FocusLock from "react-focus-lock"; import {BetaPill} from "../beta/BetaCard";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => { const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return ( return (
@ -41,17 +48,39 @@ enum Visibility {
Private, Private,
} }
const spaceNameValidator = withValidation({
rules: [
{
key: "required",
test: async ({ value }) => !!value,
invalid: () => _t("Please enter a name for the space"),
},
],
});
const SpaceCreateMenu = ({ onFinished }) => { const SpaceCreateMenu = ({ onFinished }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const [visibility, setVisibility] = useState<Visibility>(null); 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 [busy, setBusy] = useState<boolean>(false);
const onSpaceCreateClick = async () => { const [name, setName] = useState("");
const spaceNameField = useRef<Field>();
const [avatar, setAvatar] = useState<File>(null);
const [topic, setTopic] = useState<string>("");
const onSpaceCreateClick = async (e) => {
e.preventDefault();
if (busy) return; if (busy) return;
setBusy(true); setBusy(true);
// require & validate the space name field
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true });
setBusy(false);
return;
}
const initialState: IStateEvent[] = [ const initialState: IStateEvent[] = [
{ {
type: EventType.RoomHistoryVisibility, type: EventType.RoomHistoryVisibility,
@ -107,7 +136,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
if (visibility === null) { if (visibility === null) {
body = <React.Fragment> body = <React.Fragment>
<h2>{ _t("Create a space") }</h2> <h2>{ _t("Create a space") }</h2>
<p>{ _t("Spaces are new ways to group rooms and people. " + <p>{ _t("Spaces are a new way to group rooms and people. " +
"To join an existing space you'll need an invite.") }</p> "To join an existing space you'll need an invite.") }</p>
<SpaceCreateMenuType <SpaceCreateMenuType
@ -124,6 +153,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
/> />
<p>{ _t("You can change this later") }</p> <p>{ _t("You can change this later") }</p>
<SpaceFeedbackPrompt onClick={onFinished} />
</React.Fragment>; </React.Fragment>;
} else { } else {
body = <React.Fragment> body = <React.Fragment>
@ -146,9 +177,32 @@ const SpaceCreateMenu = ({ onFinished }) => {
} }
</p> </p>
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} /> <form className="mx_SpaceBasicSettings" onSubmit={onSpaceCreateClick}>
<SpaceAvatar setAvatar={setAvatar} avatarDisabled={busy} />
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name || busy}> <Field
name="spaceName"
label={_t("Name")}
autoFocus={true}
value={name}
onChange={ev => setName(ev.target.value)}
ref={spaceNameField}
onValidate={spaceNameValidator}
disabled={busy}
/>
<Field
name="spaceTopic"
element="textarea"
label={_t("Description")}
value={topic}
onChange={ev => setTopic(ev.target.value)}
rows={3}
disabled={busy}
/>
</form>
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={busy}>
{ busy ? _t("Creating...") : _t("Create") } { busy ? _t("Creating...") : _t("Create") }
</AccessibleButton> </AccessibleButton>
</React.Fragment>; </React.Fragment>;
@ -164,6 +218,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
managed={false} managed={false}
> >
<FocusLock returnFocus={true}> <FocusLock returnFocus={true}>
<BetaPill onClick={() => {
onFinished();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB,
});
}} />
{ body } { body }
</FocusLock> </FocusLock>
</ContextMenu>; </ContextMenu>;

View file

@ -23,6 +23,7 @@ import {copyPlaintext} from "../../../utils/strings";
import {sleep} from "../../../utils/promise"; import {sleep} from "../../../utils/promise";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {showRoomInviteDialog} from "../../../RoomInvite"; import {showRoomInviteDialog} from "../../../RoomInvite";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
interface IProps { interface IProps {
space: Room; space: Room;
@ -50,7 +51,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
<h3>{ _t("Share invite link") }</h3> <h3>{ _t("Share invite link") }</h3>
<span>{ copiedText }</span> <span>{ copiedText }</span>
</AccessibleButton> </AccessibleButton>
<AccessibleButton { space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
className="mx_SpacePublicShare_inviteButton" className="mx_SpacePublicShare_inviteButton"
onClick={() => { onClick={() => {
showRoomInviteDialog(space.roomId); showRoomInviteDialog(space.roomId);
@ -59,7 +60,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
> >
<h3>{ _t("Invite people") }</h3> <h3>{ _t("Invite people") }</h3>
<span>{ _t("Invite with email or username") }</span> <span>{ _t("Invite with email or username") }</span>
</AccessibleButton> </AccessibleButton> : null }
</div>; </div>;
}; };

View file

@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge"; import NotificationBadge from "../rooms/NotificationBadge";
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton"; import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton"; import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
@ -68,8 +69,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
constructor(props) { constructor(props) {
super(props); super(props);
const collapsed = SpaceTreeLevelLayoutStore.instance.getSpaceCollapsedState(
props.space.roomId,
this.props.parents,
!props.isNested, // default to collapsed for root items
);
this.state = { this.state = {
collapsed: !props.isNested, // default to collapsed for root items collapsed: collapsed,
contextMenuPosition: null, contextMenuPosition: null,
}; };
} }
@ -78,7 +85,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
if (this.props.onExpand && this.state.collapsed) { if (this.props.onExpand && this.state.collapsed) {
this.props.onExpand(); this.props.onExpand();
} }
this.setState({collapsed: !this.state.collapsed}); const newCollapsedState = !this.state.collapsed;
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
this.props.space.roomId,
this.props.parents,
newCollapsedState,
);
this.setState({collapsed: newCollapsedState});
// don't bubble up so encapsulating button for space // don't bubble up so encapsulating button for space
// doesn't get triggered // doesn't get triggered
evt.stopPropagation(); evt.stopPropagation();
@ -195,7 +209,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const userId = this.context.getUserId(); const userId = this.context.getUserId();
let inviteOption; let inviteOption;
if (this.props.space.canInvite(userId)) { if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
inviteOption = ( inviteOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton" className="mx_SpacePanel_contextMenu_inviteButton"

View file

@ -57,8 +57,8 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
}; };
private onTimeUpdate = (time: number[]) => { private onTimeUpdate = (time: number[]) => {
// Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar. // Track percentages to a general precision to avoid over-waking the component.
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1)); const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
this.setState({progress}); this.setState({progress});
}; };

43
src/effects/effect.ts Normal file
View file

@ -0,0 +1,43 @@
/*
Copyright 2020 Nurjin Jafar
Copyright 2020 Nordeck IT + Consulting GmbH.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export type Effect<TOptions extends { [key: string]: any }> = {
/**
* one or more emojis that will trigger this effect
*/
emojis: Array<string>;
/**
* the matrix message type that will trigger this effect
*/
msgType: string;
/**
* the room command to trigger this effect
*/
command: string;
/**
* a function that returns the translated description of the effect
*/
description: () => string;
/**
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
*/
fallbackMessage: () => string;
/**
* animation options
*/
options: TOptions;
}

View file

@ -15,80 +15,11 @@
limitations under the License. limitations under the License.
*/ */
import { _t, _td } from "../languageHandler"; import { _t, _td } from "../languageHandler";
import { ConfettiOptions } from "./confetti";
export type Effect<TOptions extends { [key: string]: any }> = { import { Effect } from "./effect";
/** import { FireworksOptions } from "./fireworks";
* one or more emojis that will trigger this effect import { SnowfallOptions } from "./snowfall";
*/ import { SpaceInvadersOptions } from "./spaceinvaders";
emojis: Array<string>;
/**
* the matrix message type that will trigger this effect
*/
msgType: string;
/**
* the room command to trigger this effect
*/
command: string;
/**
* a function that returns the translated description of the effect
*/
description: () => string;
/**
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
*/
fallbackMessage: () => string;
/**
* animation options
*/
options: TOptions;
}
type ConfettiOptions = {
/**
* max confetti count
*/
maxCount: number;
/**
* particle animation speed
*/
speed: number;
/**
* the confetti animation frame interval in milliseconds
*/
frameInterval: number;
/**
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
*/
alpha: number;
/**
* use gradient instead of solid particle color
*/
gradient: boolean;
};
type FireworksOptions = {
/**
* max fireworks count
*/
maxCount: number;
/**
* gravity value that firework adds to shift from it's start position
*/
gravity: number;
}
type SnowfallOptions = {
/**
* The maximum number of snowflakes to render at a given time
*/
maxCount: number;
/**
* The amount of gravity to apply to the snowflakes
*/
gravity: number;
/**
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
*/
maxDrift: number;
}
/** /**
* This configuration defines room effects that can be triggered by custom message types and emojis * This configuration defines room effects that can be triggered by custom message types and emojis
@ -131,6 +62,17 @@ export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
maxDrift: 5, maxDrift: 5,
}, },
} as Effect<SnowfallOptions>, } as Effect<SnowfallOptions>,
{
emojis: ["👾", "🌌"],
msgType: "io.element.effects.space_invaders",
command: "spaceinvaders",
description: () => _td("Sends the given message with a space themed effect"),
fallbackMessage: () => _t("sends space invaders") + " 👾",
options: {
maxCount: 50,
gravity: 0.01,
},
} as Effect<SpaceInvadersOptions>,
]; ];

View file

@ -0,0 +1,119 @@
/*
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 ICanvasEffect from '../ICanvasEffect';
import { arrayFastClone } from "../../utils/arrays";
export type SpaceInvadersOptions = {
/**
* The maximum number of invaders to render at a given time
*/
maxCount: number;
/**
* The amount of gravity to apply to the invaders
*/
gravity: number;
}
type Invader = {
x: number;
y: number;
xCol: number;
gravity: number;
}
export const DefaultOptions: SpaceInvadersOptions = {
maxCount: 50,
gravity: 0.005,
};
const KEY_FRAME_INTERVAL = 15; // 15ms, roughly
const GLYPH = "👾";
export default class SpaceInvaders implements ICanvasEffect {
private readonly options: SpaceInvadersOptions;
constructor(options: { [key: string]: any }) {
this.options = {...DefaultOptions, ...options};
}
private context: CanvasRenderingContext2D | null = null;
private particles: Array<Invader> = [];
private lastAnimationTime: number;
public isRunning: boolean;
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
if (!canvas) {
return;
}
this.context = canvas.getContext('2d');
this.particles = [];
const count = this.options.maxCount;
while (this.particles.length < count) {
this.particles.push(this.resetParticle({} as Invader, canvas.width, canvas.height));
}
this.isRunning = true;
requestAnimationFrame(this.renderLoop);
if (timeout) {
window.setTimeout(this.stop, timeout);
}
}
public stop = async () => {
this.isRunning = false;
}
private resetParticle = (particle: Invader, width: number, height: number): Invader => {
particle.x = Math.random() * width;
particle.y = Math.random() * -height;
particle.xCol = particle.x;
particle.gravity = this.options.gravity + (Math.random() * 6) + 4;
return particle;
}
private renderLoop = (): void => {
if (!this.context || !this.context.canvas) {
return;
}
if (this.particles.length === 0) {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
} else {
const timeDelta = Date.now() - this.lastAnimationTime;
if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) {
// Clear the screen first
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
this.lastAnimationTime = Date.now();
this.animateAndRenderInvaders();
}
requestAnimationFrame(this.renderLoop);
}
};
private animateAndRenderInvaders() {
if (!this.context || !this.context.canvas) {
return;
}
this.context.font = "50px Twemoji";
for (const particle of arrayFastClone(this.particles)) {
particle.y += particle.gravity;
this.context.save();
this.context.fillText(GLYPH, particle.x, particle.y);
this.context.restore();
}
}
}

View file

@ -1187,7 +1187,7 @@
"Changes your display nickname in the current room only": "Změní vaši zobrazovanou přezdívku pouze v této místnosti", "Changes your display nickname in the current room only": "Změní vaši zobrazovanou přezdívku pouze v této místnosti",
"User %(userId)s is already in the room": "Uživatel %(userId)s už je v této místnosti", "User %(userId)s is already in the room": "Uživatel %(userId)s už je v této místnosti",
"The user must be unbanned before they can be invited.": "Uživatel je vykázán, nelze ho pozvat.", "The user must be unbanned before they can be invited.": "Uživatel je vykázán, nelze ho pozvat.",
"Show read receipts sent by other users": "Zobrazovat potvrzení o přijetí", "Show read receipts sent by other users": "Zobrazovat potvrzení o přečtení",
"Scissors": "Nůžky", "Scissors": "Nůžky",
"Accept all %(invitedRooms)s invites": "Přijmout pozvání do všech těchto místností: %(invitedRooms)s", "Accept all %(invitedRooms)s invites": "Přijmout pozvání do všech těchto místností: %(invitedRooms)s",
"Change room avatar": "Změnit avatar místnosti", "Change room avatar": "Změnit avatar místnosti",
@ -3155,7 +3155,7 @@
"Invite People": "Pozvat lidi", "Invite People": "Pozvat lidi",
"Invite with email or username": "Pozvěte e-mailem nebo uživatelským jménem", "Invite with email or username": "Pozvěte e-mailem nebo uživatelským jménem",
"You can change these anytime.": "Tyto údaje můžete kdykoli změnit.", "You can change these anytime.": "Tyto údaje můžete kdykoli změnit.",
"Add some details to help people recognise it.": "Přidejte několik podrobností, aby to lidé lépe rozpoznali.", "Add some details to help people recognise it.": "Přidejte nějaké podrobnosti, aby ho lidé lépe rozpoznali.",
"Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Prostory jsou nový způsob, jak seskupovat místnosti a lidi. Chcete-li se připojit ke stávajícímu prostoru, budete potřebovat pozvánku.", "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Prostory jsou nový způsob, jak seskupovat místnosti a lidi. Chcete-li se připojit ke stávajícímu prostoru, budete potřebovat pozvánku.",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "K vašemu účtu přistupuje nové přihlášení: %(name)s (%(deviceID)s) pomocí %(ip)s", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "K vašemu účtu přistupuje nové přihlášení: %(name)s (%(deviceID)s) pomocí %(ip)s",
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "Z %(deviceName)s (%(deviceId)s) pomocí %(ip)s", "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Z %(deviceName)s (%(deviceId)s) pomocí %(ip)s",
@ -3201,7 +3201,7 @@
"Please choose a strong password": "Vyberte silné heslo", "Please choose a strong password": "Vyberte silné heslo",
"What are some things you want to discuss in %(spaceName)s?": "O kterých tématech chcete diskutovat v %(spaceName)s?", "What are some things you want to discuss in %(spaceName)s?": "O kterých tématech chcete diskutovat v %(spaceName)s?",
"Let's create a room for each of them.": "Vytvořme pro každé z nich místnost.", "Let's create a room for each of them.": "Vytvořme pro každé z nich místnost.",
"Use another login": "Použijte jiné přihlašovací jméno", "Use another login": "Použít jinou relaci",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Bez ověření nebudete mít přístup ke všem svým zprávám a ostatním se můžete zobrazit jako nedůvěryhodný.", "Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Bez ověření nebudete mít přístup ke všem svým zprávám a ostatním se můžete zobrazit jako nedůvěryhodný.",
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "Jste zde jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.", "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Jste zde jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.",
"If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Pokud vše resetujete, začnete bez důvěryhodných relací, bez důvěryhodných uživatelů a možná nebudete moci zobrazit minulé zprávy.", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Pokud vše resetujete, začnete bez důvěryhodných relací, bez důvěryhodných uživatelů a možná nebudete moci zobrazit minulé zprávy.",
@ -3225,5 +3225,16 @@
"Including %(commaSeparatedMembers)s": "Včetně %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Včetně %(commaSeparatedMembers)s",
"View all %(count)s members|one": "Zobrazit jednoho člena", "View all %(count)s members|one": "Zobrazit jednoho člena",
"View all %(count)s members|other": "Zobrazit všech %(count)s členů", "View all %(count)s members|other": "Zobrazit všech %(count)s členů",
"Failed to send": "Odeslání se nezdařilo" "Failed to send": "Odeslání se nezdařilo",
"What do you want to organise?": "Co si přejete organizovat?",
"Filter all spaces": "Filtrovat všechny prostory",
"Delete recording": "Smazat zvukovou zprávu",
"Stop the recording": "Zastavit nahrávání",
"%(count)s results in all spaces|one": "%(count)s výsledek ve všech prostorech",
"%(count)s results in all spaces|other": "%(count)s výsledků ve všech prostorech",
"Play": "Přehrát",
"Pause": "Pozastavit",
"Enter your Security Phrase a second time to confirm it.": "Zadejte bezpečnostní frázi podruhé a potvrďte ji.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Vyberte místnosti nebo konverzace, které chcete přidat. Toto je prostor pouze pro vás, nikdo nebude informován. Později můžete přidat další.",
"You have no ignored users.": "Nemáte žádné ignorované uživatele."
} }

View file

@ -88,7 +88,7 @@
"Unban": "Verbannung aufheben", "Unban": "Verbannung aufheben",
"unknown error code": "Unbekannter Fehlercode", "unknown error code": "Unbekannter Fehlercode",
"Upload avatar": "Profilbild hochladen", "Upload avatar": "Profilbild hochladen",
"Upload file": "Datei hochladen", "Upload file": "Datei senden",
"Users": "Benutzer", "Users": "Benutzer",
"Verification Pending": "Verifizierung ausstehend", "Verification Pending": "Verifizierung ausstehend",
"Video call": "Videoanruf", "Video call": "Videoanruf",
@ -114,7 +114,7 @@
"VoIP is unsupported": "VoIP wird nicht unterstützt", "VoIP is unsupported": "VoIP wird nicht unterstützt",
"You are already in a call.": "Du bist bereits in einem Gespräch.", "You are already in a call.": "Du bist bereits in einem Gespräch.",
"You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.", "You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.",
"You cannot place VoIP calls in this browser.": "VoIP-Gespräche werden von diesem Browser nicht unterstützt.", "You cannot place VoIP calls in this browser.": "Anrufe werden von diesem Browser nicht unterstützt.",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Heimserver verbunden zu sein.", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Heimserver verbunden zu sein.",
"Sun": "So", "Sun": "So",
"Mon": "Mo", "Mon": "Mo",
@ -1634,7 +1634,7 @@
"Show rooms with unread notifications first": "Räume mit ungelesenen Benachrichtigungen zuerst zeigen", "Show rooms with unread notifications first": "Räume mit ungelesenen Benachrichtigungen zuerst zeigen",
"Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen", "Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen",
"Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen", "Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte E-Mail-Adresse mit der Einmalanmeldung, um deine Identität nachzuweisen.", "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser E-Mail-Adresse durch Single Sign-on, um deine Identität nachzuweisen.",
"Single Sign On": "Einmalanmeldung", "Single Sign On": "Einmalanmeldung",
"Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen", "Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.", "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.",
@ -2951,7 +2951,7 @@
"Call failed because no webcam or microphone could not be accessed. Check that:": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon oder die Webcam zugegriffen werden konnte. Stelle sicher, dass:", "Call failed because no webcam or microphone could not be accessed. Check that:": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon oder die Webcam zugegriffen werden konnte. Stelle sicher, dass:",
"Unable to access webcam / microphone": "Auf Webcam / Mikrofon konnte nicht zugegriffen werden", "Unable to access webcam / microphone": "Auf Webcam / Mikrofon konnte nicht zugegriffen werden",
"Unable to access microphone": "Es konnte nicht auf das Mikrofon zugegriffen werden", "Unable to access microphone": "Es konnte nicht auf das Mikrofon zugegriffen werden",
"Host account on": "Benutzer*innenkonto betreiben an", "Host account on": "Konto betreiben auf",
"Hold": "Halten", "Hold": "Halten",
"Resume": "Fortsetzen", "Resume": "Fortsetzen",
"We call the places where you can host your account homeservers.": "Den Ort, an dem du dein Konto betreibst, nennen wir „Heimserver“.", "We call the places where you can host your account homeservers.": "Den Ort, an dem du dein Konto betreibst, nennen wir „Heimserver“.",
@ -3282,5 +3282,16 @@
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Falls du es wirklich willst: Es werden keine Nachrichten gelöscht. Außerdem wird die Suche, während der Index erstellt wird, etwas langsamer sein", "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Falls du es wirklich willst: Es werden keine Nachrichten gelöscht. Außerdem wird die Suche, während der Index erstellt wird, etwas langsamer sein",
"%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s Mitglieder inklusive %(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s Mitglieder inklusive %(commaSeparatedMembers)s",
"Including %(commaSeparatedMembers)s": "Inklusive%(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Inklusive%(commaSeparatedMembers)s",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Beratung mit %(transferTarget)s. <a>Übertragung zu %(transferee)s</a>" "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Beratung mit %(transferTarget)s. <a>Übertragung zu %(transferee)s</a>",
"Play": "Abspielen",
"Pause": "Pause",
"What do you want to organise?": "Was willst du organisieren?",
"Enter your Security Phrase a second time to confirm it.": "Gib dein Kennwort ein zweites Mal zur Bestätigung ein.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Wähle Räume oder Konversationen die Du hinzufügen möchtest. Dieser Bereich ist nur für Dich, niemand wird informiert. Du kannst später mehr hinzufügen.",
"Filter all spaces": "Alle Bereiche filtern",
"Delete recording": "Aufnahme löschen",
"Stop the recording": "Aufnahme stoppen",
"%(count)s results in all spaces|one": "%(count)s Ergebnis in allen Bereichen",
"%(count)s results in all spaces|other": "%(count)s Ergebnisse in allen Bereichen",
"You have no ignored users.": "Du ignorierst keine Benutzer."
} }

View file

@ -578,14 +578,6 @@
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
"Light": "Light", "Light": "Light",
"Dark": "Dark", "Dark": "Dark",
"You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:",
"Verify your other session using one of the options below.": "Verify your other session using one of the options below.",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
"Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.",
"Not Trusted": "Not Trusted",
"Manually Verify by Text": "Manually Verify by Text",
"Interactively verify by Emoji": "Interactively verify by Emoji",
"Done": "Done",
"%(displayName)s is typing …": "%(displayName)s is typing …", "%(displayName)s is typing …": "%(displayName)s is typing …",
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
@ -608,6 +600,10 @@
"See when the avatar changes in this room": "See when the avatar changes in this room", "See when the avatar changes in this room": "See when the avatar changes in this room",
"Change the avatar of your active room": "Change the avatar of your active room", "Change the avatar of your active room": "Change the avatar of your active room",
"See when the avatar changes in your active room": "See when the avatar changes in your active room", "See when the avatar changes in your active room": "See when the avatar changes in your active room",
"Kick, ban, or invite people to this room, and make you leave": "Kick, ban, or invite people to this room, and make you leave",
"See when people join, leave, or are invited to this room": "See when people join, leave, or are invited to this room",
"Kick, ban, or invite people to your active room, and make you leave": "Kick, ban, or invite people to your active room, and make you leave",
"See when people join, leave, or are invited to your active room": "See when people join, leave, or are invited to your active room",
"Send stickers to this room as you": "Send stickers to this room as you", "Send stickers to this room as you": "Send stickers to this room as you",
"See when a sticker is posted in this room": "See when a sticker is posted in this room", "See when a sticker is posted in this room": "See when a sticker is posted in this room",
"Send stickers to your active room as you": "Send stickers to your active room as you", "Send stickers to your active room as you": "Send stickers to your active room as you",
@ -785,8 +781,16 @@
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings", "Change notification settings": "Change notification settings",
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
"Spaces": "Spaces",
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.",
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.",
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages (in development)": "Send and receive voice messages (in development)", "Send and receive voice messages": "Send and receive voice messages",
"Render LaTeX maths in messages": "Render LaTeX maths in messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
"New spinner design": "New spinner design", "New spinner design": "New spinner design",
@ -880,6 +884,8 @@
"sends fireworks": "sends fireworks", "sends fireworks": "sends fireworks",
"Sends the given message with snowfall": "Sends the given message with snowfall", "Sends the given message with snowfall": "Sends the given message with snowfall",
"sends snowfall": "sends snowfall", "sends snowfall": "sends snowfall",
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
"sends space invaders": "sends space invaders",
"unknown person": "unknown person", "unknown person": "unknown person",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>", "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>", "You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
@ -996,8 +1002,9 @@
"Upload": "Upload", "Upload": "Upload",
"Name": "Name", "Name": "Name",
"Description": "Description", "Description": "Description",
"Please enter a name for the space": "Please enter a name for the space",
"Create a space": "Create a space", "Create a space": "Create a space",
"Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.", "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.",
"Public": "Public", "Public": "Public",
"Open space for anyone, best for communities": "Open space for anyone, best for communities", "Open space for anyone, best for communities": "Open space for anyone, best for communities",
"Private": "Private", "Private": "Private",
@ -1088,7 +1095,7 @@
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
"%(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)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)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)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)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 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 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 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.",
"Message search initilisation failed": "Message search initilisation failed", "Message search initialisation failed": "Message search initialisation failed",
"Connecting to integration manager...": "Connecting to integration manager...", "Connecting to integration manager...": "Connecting to integration manager...",
"Cannot connect to integration manager": "Cannot connect to integration manager", "Cannot connect to integration manager": "Cannot connect to integration manager",
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
@ -1258,7 +1265,7 @@
"Copy": "Copy", "Copy": "Copy",
"Clear cache and reload": "Clear cache and reload", "Clear cache and reload": "Clear cache and reload",
"Labs": "Labs", "Labs": "Labs",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.", "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
"Ignored/Blocked": "Ignored/Blocked", "Ignored/Blocked": "Ignored/Blocked",
"Error adding ignored user/server": "Error adding ignored user/server", "Error adding ignored user/server": "Error adding ignored user/server",
"Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.",
@ -1855,6 +1862,7 @@
"You sent a verification request": "You sent a verification request", "You sent a verification request": "You sent a verification request",
"Error decrypting video": "Error decrypting video", "Error decrypting video": "Error decrypting video",
"Error processing voice message": "Error processing voice message", "Error processing voice message": "Error processing voice message",
"Add reaction": "Add reaction",
"Show all": "Show all", "Show all": "Show all",
"Reactions": "Reactions", "Reactions": "Reactions",
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>", "<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
@ -1939,8 +1947,8 @@
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"Zoom out": "Zoom out", "Zoom out": "Zoom out",
"Zoom in": "Zoom in", "Zoom in": "Zoom in",
"Rotate Right": "Rotate Right",
"Rotate Left": "Rotate Left", "Rotate Left": "Rotate Left",
"Rotate Right": "Rotate Right",
"Download": "Download", "Download": "Download",
"Information": "Information", "Information": "Information",
"View message": "View message", "View message": "View message",
@ -2019,6 +2027,7 @@
"Home": "Home", "Home": "Home",
"Enter a server name": "Enter a server name", "Enter a server name": "Enter a server name",
"Looks good": "Looks good", "Looks good": "Looks good",
"You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list",
"Can't find this server or its room list": "Can't find this server or its room list", "Can't find this server or its room list": "Can't find this server or its room list",
"Your server": "Your server", "Your server": "Your server",
"Are you sure you want to remove <b>%(serverName)s</b>": "Are you sure you want to remove <b>%(serverName)s</b>", "Are you sure you want to remove <b>%(serverName)s</b>": "Are you sure you want to remove <b>%(serverName)s</b>",
@ -2030,14 +2039,15 @@
"Add a new server...": "Add a new server...", "Add a new server...": "Add a new server...",
"%(networkName)s rooms": "%(networkName)s rooms", "%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms", "Matrix rooms": "Matrix rooms",
"Filter your rooms and spaces": "Filter your rooms and spaces",
"Spaces": "Spaces",
"Direct Messages": "Direct Messages",
"Space selection": "Space selection",
"Add existing rooms": "Add existing rooms",
"Not all selected were added": "Not all selected were added", "Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
"Filter your rooms and spaces": "Filter your rooms and spaces",
"Feeling experimental?": "Feeling experimental?",
"You can add existing spaces to a space.": "You can add existing spaces to a space.",
"Direct Messages": "Direct Messages",
"Space selection": "Space selection",
"Add existing rooms": "Add existing rooms",
"Want to add a new room instead?": "Want to add a new room instead?", "Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room", "Create a new room": "Create a new room",
"Matrix ID": "Matrix ID", "Matrix ID": "Matrix ID",
@ -2053,6 +2063,15 @@
"Invite anyway and never warn me again": "Invite anyway and never warn me again", "Invite anyway and never warn me again": "Invite anyway and never warn me again",
"Invite anyway": "Invite anyway", "Invite anyway": "Invite anyway",
"Close dialog": "Close dialog", "Close dialog": "Close dialog",
"Beta feedback": "Beta feedback",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"%(featureName)s beta feedback": "%(featureName)s beta feedback",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
"Feedback": "Feedback",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Send feedback": "Send feedback",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.", "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.",
"Preparing to send logs": "Preparing to send logs", "Preparing to send logs": "Preparing to send logs",
"Logs sent": "Logs sent", "Logs sent": "Logs sent",
@ -2183,10 +2202,8 @@
"Comment": "Comment", "Comment": "Comment",
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.", "There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.", "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Feedback": "Feedback",
"Report a bug": "Report a bug", "Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.", "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"Send feedback": "Send feedback",
"Confirm abort of host creation": "Confirm abort of host creation", "Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort", "Abort": "Abort",
@ -2372,6 +2389,13 @@
"Summary": "Summary", "Summary": "Summary",
"Document": "Document", "Document": "Document",
"Next": "Next", "Next": "Next",
"You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:",
"Verify your other session using one of the options below.": "Verify your other session using one of the options below.",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
"Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.",
"Not Trusted": "Not Trusted",
"Manually Verify by Text": "Manually Verify by Text",
"Interactively verify by Emoji": "Interactively verify by Emoji",
"Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)",
"Upload files": "Upload files", "Upload files": "Upload files",
"Upload all": "Upload all", "Upload all": "Upload all",
@ -2464,6 +2488,11 @@
"Revoke permissions": "Revoke permissions", "Revoke permissions": "Revoke permissions",
"Move left": "Move left", "Move left": "Move left",
"Move right": "Move right", "Move right": "Move right",
"Spaces is a beta feature": "Spaces is a beta feature",
"Tap for more info": "Tap for more info",
"Beta": "Beta",
"Leave the beta": "Leave the beta",
"Join the beta": "Join the beta",
"Avatar": "Avatar", "Avatar": "Avatar",
"This room is public": "This room is public", "This room is public": "This room is public",
"Away": "Away", "Away": "Away",
@ -2597,6 +2626,7 @@
"Error whilst fetching joined communities": "Error whilst fetching joined communities", "Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Create a new community": "Create a new community", "Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"Communities are changing to Spaces": "Communities are changing to Spaces",
"Youre all caught up": "Youre all caught up", "Youre all caught up": "Youre all caught up",
"You have no visible notifications.": "You have no visible notifications.", "You have no visible notifications.": "You have no visible notifications.",
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
@ -2664,12 +2694,15 @@
"Mark as suggested": "Mark as suggested", "Mark as suggested": "Mark as suggested",
"No results found": "No results found", "No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Search names and description": "Search names and description", "Search names and descriptions": "Search names and descriptions",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room", "Create room": "Create room",
"Spaces are a beta feature.": "Spaces are a beta feature.",
"Public space": "Public space", "Public space": "Public space",
"Private space": "Private space", "Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you", "<inviter/> invites you": "<inviter/> invites you",
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "To join %(spaceName)s, turn on the <a>Spaces beta</a>",
"Welcome to <name/>": "Welcome to <name/>", "Welcome to <name/>": "Welcome to <name/>",
"Random": "Random", "Random": "Random",
"Support": "Support", "Support": "Support",
@ -2677,13 +2710,12 @@
"Failed to create initial space rooms": "Failed to create initial space rooms", "Failed to create initial space rooms": "Failed to create initial space rooms",
"Skip for now": "Skip for now", "Skip for now": "Skip for now",
"Creating rooms...": "Creating rooms...", "Creating rooms...": "Creating rooms...",
"Failed to add rooms to space": "Failed to add rooms to space",
"Adding...": "Adding...",
"What do you want to organise?": "What do you want to organise?", "What do you want to organise?": "What do you want to organise?",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
"Share %(name)s": "Share %(name)s", "Share %(name)s": "Share %(name)s",
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
"Go to my first room": "Go to my first room", "Go to my first room": "Go to my first room",
"Go to my space": "Go to my space",
"Who are you working with?": "Who are you working with?", "Who are you working with?": "Who are you working with?",
"Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s", "Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s",
"Just me": "Just me", "Just me": "Just me",
@ -2694,6 +2726,7 @@
"Inviting...": "Inviting...", "Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates", "Invite your teammates": "Invite your teammates",
"Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.",
"<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.": "<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.",
"Invite by username": "Invite by username", "Invite by username": "Invite by username",
"What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?", "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?",
"Let's create a room for each of them.": "Let's create a room for each of them.", "Let's create a room for each of them.": "Let's create a room for each of them.",
@ -2806,6 +2839,7 @@
"Room Notification": "Room Notification", "Room Notification": "Room Notification",
"Notification Autocomplete": "Notification Autocomplete", "Notification Autocomplete": "Notification Autocomplete",
"Room Autocomplete": "Room Autocomplete", "Room Autocomplete": "Room Autocomplete",
"Space Autocomplete": "Space Autocomplete",
"Users": "Users", "Users": "Users",
"User Autocomplete": "User Autocomplete", "User Autocomplete": "User Autocomplete",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.", "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.",

View file

@ -3248,5 +3248,16 @@
"%(seconds)ss left": "%(seconds)ss restantes", "%(seconds)ss left": "%(seconds)ss restantes",
"Failed to send": "No se ha podido mandar", "Failed to send": "No se ha podido mandar",
"Change server ACLs": "Cambiar los ACLs del servidor", "Change server ACLs": "Cambiar los ACLs del servidor",
"Show options to enable 'Do not disturb' mode": "Mostrar opciones para activar el modo «no molestar»" "Show options to enable 'Do not disturb' mode": "Mostrar opciones para activar el modo «no molestar»",
"Stop the recording": "Parar grabación",
"Delete recording": "Borrar grabación",
"Enter your Security Phrase a second time to confirm it.": "Escribe tu frase de seguridad de nuevo para confirmarla.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elige salas o conversaciones para añadirlas. Este espacio es solo para ti, no informaremos a nadie. Puedes añadir más más tarde.",
"What do you want to organise?": "¿Qué quieres organizar?",
"Filter all spaces": "Filtrar todos los espacios",
"%(count)s results in all spaces|one": "%(count)s resultado en todos los espacios",
"%(count)s results in all spaces|other": "%(count)s resultados en todos los espacios",
"You have no ignored users.": "No has ignorado a nadie.",
"Pause": "Pausar",
"Play": "Reproducir"
} }

View file

@ -3286,5 +3286,16 @@
"Including %(commaSeparatedMembers)s": "Sealhulgas %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Sealhulgas %(commaSeparatedMembers)s",
"View all %(count)s members|one": "Vaata üht liiget", "View all %(count)s members|one": "Vaata üht liiget",
"View all %(count)s members|other": "Vaata kõiki %(count)s liiget", "View all %(count)s members|other": "Vaata kõiki %(count)s liiget",
"Failed to send": "Saatmine ei õnnestunud" "Failed to send": "Saatmine ei õnnestunud",
"Enter your Security Phrase a second time to confirm it.": "Kinnitamiseks palun sisesta turvafraas teist korda.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Lisamiseks vali vestlusi ja jututubasid. Hetkel on see kogukonnakeskus vaid sinu jaoks ja esialgu keegi ei saa sellest teada. Teisi saad liituma kutsuda hiljem.",
"What do you want to organise?": "Mida sa soovid ette võtta?",
"Filter all spaces": "Otsi kõikides kogukonnakeskustest",
"Delete recording": "Kustuta salvestus",
"Stop the recording": "Lõpeta salvestamine",
"%(count)s results in all spaces|one": "%(count)s tulemus kõikides kogukonnakeskustes",
"%(count)s results in all spaces|other": "%(count)s tulemust kõikides kogukonnakeskustes",
"You have no ignored users.": "Sa ei ole veel kedagi eiranud.",
"Play": "Esita",
"Pause": "Peata"
} }

View file

@ -3024,7 +3024,7 @@
"Use Command + F to search": "Utilisez Commande + F pour rechercher", "Use Command + F to search": "Utilisez Commande + F pour rechercher",
"Show line numbers in code blocks": "Afficher les numéros de ligne dans les blocs de code", "Show line numbers in code blocks": "Afficher les numéros de ligne dans les blocs de code",
"Expand code blocks by default": "Développer les blocs de code par défaut", "Expand code blocks by default": "Développer les blocs de code par défaut",
"Show stickers button": "Afficher le bouton autocollants", "Show stickers button": "Afficher le bouton des autocollants",
"Use app": "Utiliser lapplication", "Use app": "Utiliser lapplication",
"Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web est expérimental sur téléphone. Pour une meilleure expérience et bénéficier des dernières fonctionnalités, utilisez notre application native gratuite.", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web est expérimental sur téléphone. Pour une meilleure expérience et bénéficier des dernières fonctionnalités, utilisez notre application native gratuite.",
"Use app for a better experience": "Utilisez une application pour une meilleure expérience", "Use app for a better experience": "Utilisez une application pour une meilleure expérience",
@ -3285,5 +3285,16 @@
"Including %(commaSeparatedMembers)s": "Dont %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Dont %(commaSeparatedMembers)s",
"View all %(count)s members|one": "Afficher le membre", "View all %(count)s members|one": "Afficher le membre",
"View all %(count)s members|other": "Afficher les %(count)s membres", "View all %(count)s members|other": "Afficher les %(count)s membres",
"Failed to send": "Échec de lenvoi" "Failed to send": "Échec de lenvoi",
"Play": "Lecture",
"Pause": "Pause",
"Enter your Security Phrase a second time to confirm it.": "Saisissez à nouveau votre phrase secrète pour la confirmer.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Choisissez des salons ou conversations à ajouter. Cest un espace rien que pour vous, personne nen sera informé. Vous pourrez en ajouter plus tard.",
"What do you want to organise?": "Que voulez-vous organiser ?",
"Filter all spaces": "Filtrer tous les espaces",
"Delete recording": "Supprimer lenregistrement",
"Stop the recording": "Arrêter lenregistrement",
"%(count)s results in all spaces|one": "%(count)s résultat dans tous les espaces",
"%(count)s results in all spaces|other": "%(count)s résultats dans tous les espaces",
"You have no ignored users.": "Vous navez ignoré personne."
} }

View file

@ -3308,5 +3308,16 @@
"Including %(commaSeparatedMembers)s": "Incluíndo a %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Incluíndo a %(commaSeparatedMembers)s",
"View all %(count)s members|one": "Ver 1 membro", "View all %(count)s members|one": "Ver 1 membro",
"View all %(count)s members|other": "Ver tódolos %(count)s membros", "View all %(count)s members|other": "Ver tódolos %(count)s membros",
"Failed to send": "Fallou o envío" "Failed to send": "Fallou o envío",
"Enter your Security Phrase a second time to confirm it.": "Escribe a túa Frase de Seguridade por segunda vez para confirmala.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elixe salas ou conversas para engadilas. Este é un espazo para ti, ninguén será notificado. Podes engadir máis posteriormente.",
"What do you want to organise?": "Que queres organizar?",
"Filter all spaces": "Filtrar os espazos",
"Delete recording": "Eliminar a gravación",
"Stop the recording": "Deter a gravación",
"%(count)s results in all spaces|one": "%(count)s resultado en tódolos espazos",
"%(count)s results in all spaces|other": "%(count)s resultados en tódolos espazos",
"You have no ignored users.": "Non tes usuarias ignoradas.",
"Play": "Reproducir",
"Pause": "Deter"
} }

View file

@ -3303,5 +3303,16 @@
"Including %(commaSeparatedMembers)s": "Beleértve: %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Beleértve: %(commaSeparatedMembers)s",
"View all %(count)s members|one": "1 résztvevő megmutatása", "View all %(count)s members|one": "1 résztvevő megmutatása",
"View all %(count)s members|other": "Az összes %(count)s résztvevő megmutatása", "View all %(count)s members|other": "Az összes %(count)s résztvevő megmutatása",
"Failed to send": "Küldés sikertelen" "Failed to send": "Küldés sikertelen",
"Enter your Security Phrase a second time to confirm it.": "A megerősítéshez adja meg a biztonsági jelmondatot még egyszer.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Válassz szobákat vagy beszélgetéseket amit hozzáadhat. Ez csak az ön tere, senki nem lesz értesítve. Továbbiakat később is hozzáadhat.",
"What do you want to organise?": "Mit szeretne megszervezni?",
"Filter all spaces": "Minden tér szűrése",
"Delete recording": "Felvétel törlése",
"Stop the recording": "Felvétel megállítása",
"%(count)s results in all spaces|one": "%(count)s találat van az összes térben",
"%(count)s results in all spaces|other": "%(count)s találat a terekben",
"You have no ignored users.": "Nincs figyelmen kívül hagyott felhasználó.",
"Play": "Lejátszás",
"Pause": "Szünet"
} }

View file

@ -3308,5 +3308,16 @@
"Including %(commaSeparatedMembers)s": "Inclusi %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Inclusi %(commaSeparatedMembers)s",
"View all %(count)s members|one": "Vedi 1 membro", "View all %(count)s members|one": "Vedi 1 membro",
"View all %(count)s members|other": "Vedi tutti i %(count)s membri", "View all %(count)s members|other": "Vedi tutti i %(count)s membri",
"Failed to send": "Invio fallito" "Failed to send": "Invio fallito",
"Enter your Security Phrase a second time to confirm it.": "Inserisci di nuovo la password di sicurezza per confermarla.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Scegli le stanze o le conversazioni da aggiungere. Questo è uno spazio solo per te, nessuno ne saprà nulla. Puoi aggiungerne altre in seguito.",
"What do you want to organise?": "Cosa vuoi organizzare?",
"Filter all spaces": "Filtra tutti gli spazi",
"Delete recording": "Elimina registrazione",
"Stop the recording": "Ferma la registrazione",
"%(count)s results in all spaces|one": "%(count)s risultato in tutti gli spazi",
"%(count)s results in all spaces|other": "%(count)s risultati in tutti gli spazi",
"You have no ignored users.": "Non hai utenti ignorati.",
"Play": "Riproduci",
"Pause": "Pausa"
} }

View file

@ -1517,7 +1517,7 @@
"Explore rooms": "Gesprekken ontdekken", "Explore rooms": "Gesprekken ontdekken",
"Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen", "Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen",
"Clear cache and reload": "Cache wissen en herladen", "Clear cache and reload": "Cache wissen en herladen",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit is onherroepelijk. Wilt u doorgaan?", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit kan niet ongedaan gemaakt worden. Wilt u doorgaan?",
"Remove %(count)s messages|one": "1 bericht verwijderen", "Remove %(count)s messages|one": "1 bericht verwijderen",
"%(count)s unread messages including mentions.|other": "%(count)s ongelezen berichten, inclusief vermeldingen.", "%(count)s unread messages including mentions.|other": "%(count)s ongelezen berichten, inclusief vermeldingen.",
"%(count)s unread messages.|other": "%(count)s ongelezen berichten.", "%(count)s unread messages.|other": "%(count)s ongelezen berichten.",
@ -1932,7 +1932,7 @@
"Jump to first unread room.": "Ga naar het eerste ongelezen gesprek.", "Jump to first unread room.": "Ga naar het eerste ongelezen gesprek.",
"Jump to first invite.": "Ga naar de eerste uitnodiging.", "Jump to first invite.": "Ga naar de eerste uitnodiging.",
"Session verified": "Sessie geverifieerd", "Session verified": "Sessie geverifieerd",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. Ze heeft nu toegang tot uw versleutelde berichten, en de sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. U heeft nu toegang tot uw versleutelde berichten, en deze sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.",
"Your new session is now verified. Other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. Ze zal voor andere gebruikers als vertrouwd gemarkeerd worden.", "Your new session is now verified. Other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. Ze zal voor andere gebruikers als vertrouwd gemarkeerd worden.",
"Without completing security on this session, it wont have access to encrypted messages.": "Als u de beveiliging van deze sessie niet vervolledigt, zal ze geen toegang hebben tot uw versleutelde berichten.", "Without completing security on this session, it wont have access to encrypted messages.": "Als u de beveiliging van deze sessie niet vervolledigt, zal ze geen toegang hebben tot uw versleutelde berichten.",
"Go Back": "Terugkeren", "Go Back": "Terugkeren",
@ -2366,7 +2366,7 @@
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "De beheerder van uw server heeft eind-tot-eind-versleuteling standaard uitgeschakeld in alle privégesprekken en directe gesprekken.", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "De beheerder van uw server heeft eind-tot-eind-versleuteling standaard uitgeschakeld in alle privégesprekken en directe gesprekken.",
"Scroll to most recent messages": "Spring naar meest recente bericht", "Scroll to most recent messages": "Spring naar meest recente bericht",
"The authenticity of this encrypted message can't be guaranteed on this device.": "De echtheid van dit versleutelde bericht kan op dit apparaat niet worden gegarandeerd.", "The authenticity of this encrypted message can't be guaranteed on this device.": "De echtheid van dit versleutelde bericht kan op dit apparaat niet worden gegarandeerd.",
"To link to this room, please add an address.": "Voeg een adres toe om naar deze kamer te verwijzen.", "To link to this room, please add an address.": "Voeg een adres toe om naar dit gesprek te kunnen verwijzen.",
"Remove messages sent by others": "Berichten van anderen verwijderen", "Remove messages sent by others": "Berichten van anderen verwijderen",
"Privacy": "Privacy", "Privacy": "Privacy",
"Keyboard Shortcuts": "Sneltoetsen", "Keyboard Shortcuts": "Sneltoetsen",
@ -3170,7 +3170,7 @@
"Share decryption keys for room history when inviting users": "Deel ontsleutelsleutels voor de gespreksgeschiedenis wanneer u personen uitnodigd", "Share decryption keys for room history when inviting users": "Deel ontsleutelsleutels voor de gespreksgeschiedenis wanneer u personen uitnodigd",
"Send and receive voice messages (in development)": "Verstuur en ontvang audioberichten (in ontwikkeling)", "Send and receive voice messages (in development)": "Verstuur en ontvang audioberichten (in ontwikkeling)",
"%(deviceId)s from %(ip)s": "%(deviceId)s van %(ip)s", "%(deviceId)s from %(ip)s": "%(deviceId)s van %(ip)s",
"Review to ensure your account is safe": "Controleer om u te verzekeren dat uw account veilig is", "Review to ensure your account is safe": "Controleer ze voor de zekerheid dat uw account veilig is",
"Sends the given message as a spoiler": "Verstuurt het bericht als een spoiler", "Sends the given message as a spoiler": "Verstuurt het bericht als een spoiler",
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "U bent de enige persoon hier. Als u weggaat, zal niemand in de toekomst kunnen toetreden, u ook niet.", "You are the only person here. If you leave, no one will be able to join in the future, including you.": "U bent de enige persoon hier. Als u weggaat, zal niemand in de toekomst kunnen toetreden, u ook niet.",
"If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.",
@ -3192,7 +3192,18 @@
"%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s leden inclusief %(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s leden inclusief %(commaSeparatedMembers)s",
"Including %(commaSeparatedMembers)s": "Inclusief %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Inclusief %(commaSeparatedMembers)s",
"View all %(count)s members|one": "Bekijk 1 lid", "View all %(count)s members|one": "1 lid bekijken",
"View all %(count)s members|other": "Bekijk alle %(count)s leden", "View all %(count)s members|other": "Bekijk alle %(count)s leden",
"Failed to send": "Verzenden is mislukt" "Failed to send": "Verzenden is mislukt",
"Enter your Security Phrase a second time to confirm it.": "Voor uw veiligheidswachtwoord een tweede keer in om het te bevestigen.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Kies een gesprek om hem toe te voegen. Dit is een space voor u, niemand zal hiervan een melding krijgen. U kan er later meer toevoegen.",
"What do you want to organise?": "Wat wilt u organiseren?",
"Filter all spaces": "Alle spaces filteren",
"Delete recording": "Opname verwijderen",
"Stop the recording": "Opname stoppen",
"%(count)s results in all spaces|one": "%(count)s resultaat in alle spaces",
"%(count)s results in all spaces|other": "%(count)s resultaten in alle spaces",
"You have no ignored users.": "U heeft geen gebruiker genegeerd.",
"Play": "Afspelen",
"Pause": "Pauze"
} }

View file

@ -2265,5 +2265,11 @@
"There was an error finding this widget.": "Wystąpił błąd podczas próby odnalezienia tego widżetu.", "There was an error finding this widget.": "Wystąpił błąd podczas próby odnalezienia tego widżetu.",
"Active Widgets": "Aktywne widżety", "Active Widgets": "Aktywne widżety",
"Encryption not enabled": "Nie włączono szyfrowania", "Encryption not enabled": "Nie włączono szyfrowania",
"Encryption enabled": "Włączono szyfrowanie" "Encryption enabled": "Włączono szyfrowanie",
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Twój serwer domowy był nieosiągalny i nie mógł Cię zalogować. Spróbuj ponownie. Jeśli to się powtórzy, skontaktuj się z administratorem swojego serwera.",
"Try again": "Spróbuj ponownie",
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Poprosiliśmy przeglądarkę o zapamiętanie, z którego serwera głównego korzystasz, aby umożliwić Ci logowanie, ale niestety Twoja przeglądarka o tym zapomniała. Przejdź do strony logowania i spróbuj ponownie.",
"We couldn't log you in": "Nie mogliśmy Cię zalogować",
"You're already in a call with this person.": "Prowadzisz już rozmowę z tą osobą.",
"Already in call": "Już dzwoni"
} }

View file

@ -3294,5 +3294,16 @@
"Including %(commaSeparatedMembers)s": "Prfshi %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Prfshi %(commaSeparatedMembers)s",
"View all %(count)s members|one": "Shihni 1 anëtar", "View all %(count)s members|one": "Shihni 1 anëtar",
"View all %(count)s members|other": "Shihni krejt %(count)s anëtarët", "View all %(count)s members|other": "Shihni krejt %(count)s anëtarët",
"Failed to send": "Su arrit të dërgohet" "Failed to send": "Su arrit të dërgohet",
"Enter your Security Phrase a second time to confirm it.": "Jepni Frazën tuaj të Sigurisë edhe një herë, për ta ripohuar.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Zgjidhni dhoma ose biseda që të shtohen. Kjo është thjesht një hapësirë për ju, sdo ta dijë kush tjetër. Mund të shtoni të tjerë më vonë.",
"What do you want to organise?": doni të sistemoni?",
"Filter all spaces": "Filtro krejt hapësirat",
"Delete recording": "Fshije regjistrimin",
"Stop the recording": "Ndale regjistrimin",
"%(count)s results in all spaces|one": "%(count)s përfundim në krejt hapësirat",
"%(count)s results in all spaces|other": "%(count)s përfundime në krejt hapësirat",
"You have no ignored users.": "Skeni përdorues të shpërfillur.",
"Play": "Luaje",
"Pause": "Ndalesë"
} }

View file

@ -3239,5 +3239,16 @@
"%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s medlemmar inklusive %(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s medlemmar inklusive %(commaSeparatedMembers)s",
"Including %(commaSeparatedMembers)s": "Inklusive %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Inklusive %(commaSeparatedMembers)s",
"Failed to send": "Misslyckades att skicka" "Failed to send": "Misslyckades att skicka",
"Enter your Security Phrase a second time to confirm it.": "Ange din säkerhetsfras igen för att bekräfta den.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Välj rum eller konversationer att lägga till. Detta är bara ett utrymmer för dig, ingen kommer att informeras. Du kan lägga till fler senare.",
"What do you want to organise?": "Vad vill du organisera?",
"Filter all spaces": "Filtrera alla utrymmen",
"Delete recording": "Radera inspelningen",
"Stop the recording": "Stoppa inspelningen",
"%(count)s results in all spaces|one": "%(count)s resultat i alla utrymmen",
"%(count)s results in all spaces|other": "%(count)s resultat i alla utrymmen",
"You have no ignored users.": "Du har inga ignorerade användare.",
"Play": "Spela",
"Pause": "Pausa"
} }

View file

@ -3311,5 +3311,16 @@
"Including %(commaSeparatedMembers)s": "包含 %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "包含 %(commaSeparatedMembers)s",
"View all %(count)s members|one": "檢視 1 個成員", "View all %(count)s members|one": "檢視 1 個成員",
"View all %(count)s members|other": "檢視全部 %(count)s 個成員", "View all %(count)s members|other": "檢視全部 %(count)s 個成員",
"Failed to send": "傳送失敗" "Failed to send": "傳送失敗",
"Enter your Security Phrase a second time to confirm it.": "再次輸入您的安全密語以進行確認。",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "挑選要新增的聊天室或對話。這是專屬於您的空間,不會有人被通知。您稍後可以再新增更多。",
"What do you want to organise?": "您想要整理什麼?",
"Filter all spaces": "過濾所有空間",
"Delete recording": "刪除錄製",
"Stop the recording": "停止錄製",
"%(count)s results in all spaces|one": "所有空間中有 %(count)s 個結果",
"%(count)s results in all spaces|other": "所有空間中有 %(count)s 個結果",
"You have no ignored users.": "您沒有忽略的使用者。",
"Play": "播放",
"Pause": "暫停"
} }

17
src/identifiers.ts Normal file
View file

@ -0,0 +1,17 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const ELEMENT_CLIENT_ID = "io.element.web";

View file

@ -38,7 +38,6 @@ export default class EventIndex extends EventEmitter {
this._eventsPerCrawl = 100; this._eventsPerCrawl = 100;
this._crawler = null; this._crawler = null;
this._currentCheckpoint = null; this._currentCheckpoint = null;
this.liveEventsForIndex = new Set();
} }
async init() { async init() {
@ -188,16 +187,12 @@ export default class EventIndex extends EventEmitter {
return; return;
} }
// If the event is not yet decrypted mark it for the
// Event.decrypted callback.
if (ev.isBeingDecrypted()) { if (ev.isBeingDecrypted()) {
const eventId = ev.getId(); // XXX: Private member access
this.liveEventsForIndex.add(eventId); await ev._decryptionPromise;
} else {
// If the event is decrypted or is unencrypted add it to the
// index now.
await this.addLiveEventToIndex(ev);
} }
await this.addLiveEventToIndex(ev);
} }
onRoomStateEvent = async (ev, state) => { onRoomStateEvent = async (ev, state) => {
@ -216,10 +211,7 @@ export default class EventIndex extends EventEmitter {
* listener, if so queues it up to be added to the index. * listener, if so queues it up to be added to the index.
*/ */
onEventDecrypted = async (ev, err) => { onEventDecrypted = async (ev, err) => {
const eventId = ev.getId();
// If the event isn't in our live event set, ignore it. // If the event isn't in our live event set, ignore it.
if (!this.liveEventsForIndex.delete(eventId)) return;
if (err) return; if (err) return;
await this.addLiveEventToIndex(ev); await this.addLiveEventToIndex(ev);
} }
@ -523,18 +515,23 @@ export default class EventIndex extends EventEmitter {
} }
}); });
const decryptionPromises = []; const decryptionPromises = matrixEvents
.filter(event => event.isEncrypted())
matrixEvents.forEach(ev => { .map(event => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { if (event.shouldAttemptDecryption()) {
// TODO the decryption promise is a private property, this return event.attemptDecryption(client._crypto, {
// should either be made public or we should convert the isRetry: true,
// event that gets fired when decryption is done into a emit: false,
// promise using the once event emitter method: });
// https://nodejs.org/api/events.html#events_events_once_emitter_name } else {
decryptionPromises.push(ev._decryptionPromise); // TODO the decryption promise is a private property, this
} // should either be made public or we should convert the
}); // event that gets fired when decryption is done into a
// promise using the once event emitter method:
// https://nodejs.org/api/events.html#events_events_once_emitter_name
return event._decryptionPromise;
}
});
// Let us wait for all the events to get decrypted. // Let us wait for all the events to get decrypted.
await Promise.all(decryptionPromises); await Promise.all(decryptionPromises);

View file

@ -56,6 +56,15 @@ export function newTranslatableError(message: string) {
return error; return error;
} }
export function getUserLanguage(): string {
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
if (language) {
return language;
} else {
return normalizeLanguageKey(getLanguageFromBrowser());
}
}
// Function which only purpose is to mark that a string is translatable // Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings // Does not actually do anything. It's helpful for automatic extraction of translatable strings
export function _td(s: string): string { export function _td(s: string): string {

View file

@ -254,11 +254,15 @@ matrixLinkify.options = {
target: function(href, type) { target: function(href, type) {
if (type === 'url') { if (type === 'url') {
const transformed = tryTransformPermalinkToLocalHref(href); try {
if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) { const transformed = tryTransformPermalinkToLocalHref(href);
return null; if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) {
} else { return null;
return '_blank'; } else {
return '_blank';
}
} catch (e) {
// malformed URI
} }
} }
return null; return null;

View file

@ -73,7 +73,9 @@ class ConsoleLogger {
// Convert objects and errors to helpful things // Convert objects and errors to helpful things
args = args.map((arg) => { args = args.map((arg) => {
if (arg instanceof Error) { if (arg instanceof DOMException) {
return arg.message + ` (${arg.name} | ${arg.code}) ` + (arg.stack ? `\n${arg.stack}` : '');
} else if (arg instanceof Error) {
return arg.message + (arg.stack ? `\n${arg.stack}` : ''); return arg.message + (arg.stack ? `\n${arg.stack}` : '');
} else if (typeof (arg) === 'object') { } else if (typeof (arg) === 'object') {
try { try {

View file

@ -28,6 +28,7 @@ import * as rageshake from './rageshake';
// polyfill textencoder if necessary // polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8'; import * as TextEncodingUtf8 from 'text-encoding-utf-8';
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import SdkConfig from "../SdkConfig";
let TextEncoder = window.TextEncoder; let TextEncoder = window.TextEncoder;
if (!TextEncoder) { if (!TextEncoder) {
TextEncoder = TextEncodingUtf8.TextEncoder; TextEncoder = TextEncodingUtf8.TextEncoder;
@ -268,6 +269,25 @@ function uint8ToString(buf: Buffer) {
return out; return out;
} }
export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) {
let version = "UNKNOWN";
try {
version = await PlatformPeg.get().getAppVersion();
} catch (err) {} // PlatformPeg already logs this.
const body = new FormData();
body.append("label", label);
body.append("text", comment);
body.append("can_contact", canContact ? "yes" : "no");
body.append("app", "element-web");
body.append("version", version);
body.append("platform", PlatformPeg.get().getHumanReadableName());
body.append("user_id", MatrixClientPeg.get()?.getUserId());
await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
}
function _submitReport(endpoint: string, body: FormData, progressCallback: (string) => void) { function _submitReport(endpoint: string, body: FormData, progressCallback: (string) => void) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();

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