Merge branch 'develop' into reorganize-preferences

pull/21833/head
Šimon Brandner 2021-05-22 17:25:04 +02:00
commit 7e165384fd
No known key found for this signature in database
GPG Key ID: 9760693FDD98A790
292 changed files with 8197 additions and 2820 deletions

View File

@ -15,7 +15,6 @@ module.exports = {
"prefer-promise-reject-errors": "off",
"no-async-promise-executor": "off",
"quotes": "off",
"indent": "off",
},
overrides: [{

View File

@ -1,3 +1,225 @@
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)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0)
* Upgrade to JS SDK 10.1.0
* [Release] Don't use the event's metadata to calc the scale of an image
[\#6004](https://github.com/matrix-org/matrix-react-sdk/pull/6004)
Changes in [3.20.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0-rc.1) (2021-05-04)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0...v3.20.0-rc.1)
* Upgrade to JS SDK 10.1.0-rc.1
* Translations update from Weblate
[\#5966](https://github.com/matrix-org/matrix-react-sdk/pull/5966)
* Fix more space panel layout and hover behaviour issues
[\#5965](https://github.com/matrix-org/matrix-react-sdk/pull/5965)
* Fix edge case with space panel alignment with subspaces on ff
[\#5964](https://github.com/matrix-org/matrix-react-sdk/pull/5964)
* Fix saving room pill part to history
[\#5951](https://github.com/matrix-org/matrix-react-sdk/pull/5951)
* Generate room preview even when minimized
[\#5948](https://github.com/matrix-org/matrix-react-sdk/pull/5948)
* Another change from recovery passphrase to Security Phrase
[\#5934](https://github.com/matrix-org/matrix-react-sdk/pull/5934)
* Sort rooms in the add existing to space dialog based on recency
[\#5943](https://github.com/matrix-org/matrix-react-sdk/pull/5943)
* Inhibit sending RR when context switching to a room
[\#5944](https://github.com/matrix-org/matrix-react-sdk/pull/5944)
* Prevent room list keyboard handling from landing focus on hidden nodes
[\#5950](https://github.com/matrix-org/matrix-react-sdk/pull/5950)
* Make the text filter search all spaces instead of just the selected one
[\#5942](https://github.com/matrix-org/matrix-react-sdk/pull/5942)
* Enable indent rule and fix indent
[\#5931](https://github.com/matrix-org/matrix-react-sdk/pull/5931)
* Prevent peeking members from reacting
[\#5946](https://github.com/matrix-org/matrix-react-sdk/pull/5946)
* Disallow inline display maths
[\#5939](https://github.com/matrix-org/matrix-react-sdk/pull/5939)
* Space creation prompt user to add existing rooms for "Just Me" spaces
[\#5923](https://github.com/matrix-org/matrix-react-sdk/pull/5923)
* Add test coverage collection script
[\#5937](https://github.com/matrix-org/matrix-react-sdk/pull/5937)
* Fix joining room using via servers regression
[\#5936](https://github.com/matrix-org/matrix-react-sdk/pull/5936)
* Revert "Fixes the two Todays problem in Redaction"
[\#5938](https://github.com/matrix-org/matrix-react-sdk/pull/5938)
* Handle encoded matrix URLs
[\#5903](https://github.com/matrix-org/matrix-react-sdk/pull/5903)
* Render ignored users setting regardless of if there are any
[\#5860](https://github.com/matrix-org/matrix-react-sdk/pull/5860)
* Fix inserting trailing colon after mention/pill
[\#5830](https://github.com/matrix-org/matrix-react-sdk/pull/5830)
* Fixes the two Todays problem in Redaction
[\#5917](https://github.com/matrix-org/matrix-react-sdk/pull/5917)
* Fix page up/down scrolling only half a page
[\#5920](https://github.com/matrix-org/matrix-react-sdk/pull/5920)
* Voice messages: Composer controls
[\#5935](https://github.com/matrix-org/matrix-react-sdk/pull/5935)
* Support MSC3086 asserted identity
[\#5886](https://github.com/matrix-org/matrix-react-sdk/pull/5886)
* Handle possible edge case with getting stuck in "unsent messages" bar
[\#5930](https://github.com/matrix-org/matrix-react-sdk/pull/5930)
* Fix suggested rooms not showing up regression from room list optimisation
[\#5932](https://github.com/matrix-org/matrix-react-sdk/pull/5932)
* Broadcast language change to ElectronPlatform
[\#5913](https://github.com/matrix-org/matrix-react-sdk/pull/5913)
* Fix VoIP PIP frame color
[\#5701](https://github.com/matrix-org/matrix-react-sdk/pull/5701)
* Convert some Flow-typed files to TypeScript
[\#5912](https://github.com/matrix-org/matrix-react-sdk/pull/5912)
* Initial SpaceStore tests work
[\#5906](https://github.com/matrix-org/matrix-react-sdk/pull/5906)
* Fix issues with space hierarchy in layout and with incompatible servers
[\#5926](https://github.com/matrix-org/matrix-react-sdk/pull/5926)
* Scale all mxc thumbs using device pixel ratio for hidpi
[\#5928](https://github.com/matrix-org/matrix-react-sdk/pull/5928)
* Fix add existing to space dialog no longer showing rooms for public spaces
[\#5918](https://github.com/matrix-org/matrix-react-sdk/pull/5918)
* Disable spaces context switching for when exploring a space
[\#5924](https://github.com/matrix-org/matrix-react-sdk/pull/5924)
* Autofocus search box in the add existing to space dialog
[\#5921](https://github.com/matrix-org/matrix-react-sdk/pull/5921)
* Use label element in add existing to space dialog for easier hit target
[\#5922](https://github.com/matrix-org/matrix-react-sdk/pull/5922)
* Dynamic max and min zoom in the new ImageView
[\#5916](https://github.com/matrix-org/matrix-react-sdk/pull/5916)
* Improve message error states
[\#5897](https://github.com/matrix-org/matrix-react-sdk/pull/5897)
* Check for null room in `VisibilityProvider`
[\#5914](https://github.com/matrix-org/matrix-react-sdk/pull/5914)
* Add unit tests for various collection-based utility functions
[\#5910](https://github.com/matrix-org/matrix-react-sdk/pull/5910)
* Spaces visual fixes
[\#5909](https://github.com/matrix-org/matrix-react-sdk/pull/5909)
* Remove reliance on DOM API to generated message preview
[\#5908](https://github.com/matrix-org/matrix-react-sdk/pull/5908)
* Expand upon voice message event & include overall waveform
[\#5888](https://github.com/matrix-org/matrix-react-sdk/pull/5888)
* Use floats for image background opacity
[\#5905](https://github.com/matrix-org/matrix-react-sdk/pull/5905)
* Show invites to spaces at the top of the space panel
[\#5902](https://github.com/matrix-org/matrix-react-sdk/pull/5902)
* Improve edge cases with spaces context switching
[\#5899](https://github.com/matrix-org/matrix-react-sdk/pull/5899)
* Fix spaces notification dots wrongly including upgraded (hidden) rooms
[\#5900](https://github.com/matrix-org/matrix-react-sdk/pull/5900)
* Iterate the spaces face pile design
[\#5898](https://github.com/matrix-org/matrix-react-sdk/pull/5898)
* Fix alignment issue with nested spaces being cut off wrong
[\#5890](https://github.com/matrix-org/matrix-react-sdk/pull/5890)
Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0)

View File

@ -28,7 +28,7 @@ Platform Targets:
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
* Mobile Web is not currently a target platform - instead please use the native
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
(https://github.com/matrix-org/matrix-android-sdk) SDKs.
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
All code lands on the `develop` branch - `master` is only used for stable releases.
**Please file PRs against `develop`!!**

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",
"version": "3.19.0",
"version": "3.21.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -58,7 +58,7 @@
"blueimp-canvas-to-blob": "^3.28.0",
"browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3",
"cheerio": "^1.0.0-rc.5",
"cheerio": "^1.0.0-rc.9",
"classnames": "^2.2.6",
"commonmark": "^0.29.3",
"counterpart": "^0.18.6",
@ -80,7 +80,7 @@
"linkifyjs": "^2.1.9",
"lodash": "^4.17.20",
"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",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
@ -97,7 +97,7 @@
"react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.1",
"rfc4648": "^1.4.0",
"sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db",
"sanitize-html": "^2.3.2",
"tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0",
@ -132,11 +132,12 @@
"@types/modernizr": "^3.5.3",
"@types/node": "^14.14.22",
"@types/pako": "^1.0.1",
"@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "^16.9",
"@types/react-dom": "^16.9.10",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^1.27.0",
"@types/sanitize-html": "^2.3.1",
"@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
@ -185,7 +186,10 @@
],
"moduleNameMapper": {
"\\.(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": [
"/node_modules/(?!matrix-js-sdk).+$"

View File

@ -54,6 +54,7 @@
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_PulsedAvatar.scss";
@import "./views/avatars/_WidgetAvatar.scss";
@import "./views/beta/_BetaCard.scss";
@import "./views/context_menus/_CallContextMenu.scss";
@import "./views/context_menus/_IconizedContextMenu.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@ -62,6 +63,7 @@
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_BetaFeedbackDialog.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@ -96,6 +98,7 @@
@import "./views/dialogs/_SpaceSettingsDialog.scss";
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UntrustedDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";
@ -161,6 +164,7 @@
@import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MVoiceMessageBody.scss";
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_MjolnirBody.scss";
@ -236,6 +240,7 @@
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss";
@import "./views/settings/tabs/user/_GeneralUserSettingsTab.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/_NotificationUserSettingsTab.scss";
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";

View File

@ -115,8 +115,3 @@ limitations under the License.
border-top: 8px solid $menu-bg-color;
border-right: 8px solid transparent;
}
.mx_ContextualMenu_spinner {
display: block;
margin: 0 auto;
}

View File

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

View File

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

View File

@ -79,6 +79,10 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceItem {
display: inline-flex;
flex-flow: wrap;
&.mx_SpaceItem_narrow {
align-self: baseline;
}
}
.mx_SpaceItem.collapsed {
@ -233,7 +237,6 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpacePanel_badgeContainer {
position: absolute;
height: 16px;
// Create a flexbox to make aligning dot badges easier
display: flex;
@ -245,23 +248,37 @@ $activeBorderColor: $secondary-fg-color;
.mx_NotificationBadge_dot {
// make the smaller dot occupy the same width for centering
margin-left: 7px;
margin-right: 7px;
margin: 0 7px;
}
}
&.collapsed {
.mx_SpaceButton {
.mx_SpacePanel_badgeContainer {
right: -3px;
top: -3px;
right: 0;
top: 0;
.mx_NotificationBadge {
background-clip: padding-box;
}
.mx_NotificationBadge_dot {
margin: 0 -1px 0 0;
border: 3px solid $groupFilterPanel-bg-color;
}
.mx_NotificationBadge_2char,
.mx_NotificationBadge_3char {
margin: -5px -5px 0 0;
border: 2px solid $groupFilterPanel-bg-color;
}
}
&.mx_SpaceButton_active .mx_SpacePanel_badgeContainer {
// when we draw the selection border we move the relative bounds of our parent
// so update our position within the bounds of the parent to maintain position overall
right: -6px;
top: -6px;
right: -3px;
top: -3px;
}
}
}
@ -275,7 +292,7 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceButton:hover,
.mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen {
&:not(.mx_SpaceButton_home) {
&:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) {
// Hide the badge container on hover because it'll be a menu button
.mx_SpacePanel_badgeContainer {
width: 0;

View File

@ -86,7 +86,7 @@ limitations under the License.
color: $primary-fg-color;
.mx_AccessibleButton {
padding: 2px 8px;
padding: 4px 12px;
font-weight: normal;
& + .mx_AccessibleButton {
@ -94,6 +94,11 @@ limitations under the License.
}
}
.mx_AccessibleButton_kind_danger_outline,
.mx_AccessibleButton_kind_primary_outline {
padding: 3px 12px; // to account for the 1px border
}
> span {
margin-left: auto;
}
@ -246,11 +251,17 @@ limitations under the License.
grid-row: 1/3;
.mx_AccessibleButton {
padding: 8px 18px;
line-height: $font-24px;
padding: 4px 16px;
display: inline-block;
visibility: hidden;
}
.mx_AccessibleButton_kind_danger_outline,
.mx_AccessibleButton_kind_primary_outline {
padding: 3px 16px; // to account for the 1px border
}
.mx_Checkbox {
display: inline-flex;
vertical-align: middle;

View File

@ -93,6 +93,10 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
&:not(.mx_SpaceRoomView_landing) .mx_SpaceFeedbackPrompt {
width: $SpaceRoomViewInnerWidth;
}
.mx_SpaceRoomView_buttons {
display: block;
margin-top: 44px;
@ -103,6 +107,10 @@ $SpaceRoomViewInnerWidth: 428px;
padding: 8px 22px;
margin-left: 16px;
}
input.mx_AccessibleButton {
border: none; // override default styles
}
}
.mx_Field {
@ -133,6 +141,44 @@ $SpaceRoomViewInnerWidth: 428px;
box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color;
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 {
display: flex;
@ -238,7 +284,8 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SpaceRoomView_landing_inviteButton {
position: relative;
padding-left: 40px;
padding: 4px 18px 4px 40px;
line-height: $font-24px;
height: min-content;
&::before {
@ -254,6 +301,27 @@ $SpaceRoomViewInnerWidth: 428px;
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
.mx_SpaceRoomView_landing_settingsButton {
position: relative;
margin-left: 16px;
width: 24px;
height: 24px;
&::before {
position: absolute;
content: "";
left: 0;
top: 0;
height: 24px;
width: 24px;
background: $tertiary-fg-color;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/settings.svg');
}
}
}
.mx_SpaceRoomView_landing_topic {
@ -268,87 +336,22 @@ $SpaceRoomViewInnerWidth: 428px;
background-color: $groupFilterPanel-bg-color;
}
.mx_SpaceRoomView_landing_adminButtons {
margin-top: 24px;
.mx_AccessibleButton {
position: relative;
width: 160px;
height: 124px;
box-sizing: border-box;
padding: 72px 16px 0;
border-radius: 12px;
border: 1px solid $input-border-color;
margin-right: 28px;
margin-bottom: 20px;
font-size: $font-14px;
display: inline-block;
vertical-align: bottom;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: rgba(141, 151, 165, 0.1);
}
&::before, &::after {
position: absolute;
content: "";
left: 16px;
top: 16px;
height: 40px;
width: 40px;
border-radius: 20px;
}
&::after {
mask-position: center;
mask-size: 30px;
mask-repeat: no-repeat;
background: #ffffff; // white icon fill
}
&.mx_SpaceRoomView_landing_addButton {
&::before {
background-color: #ac3ba8;
}
&::after {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
}
&.mx_SpaceRoomView_landing_createButton {
&::before {
background-color: #368bd6;
}
&::after {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
}
&.mx_SpaceRoomView_landing_settingsButton {
&::before {
background-color: #5c56f5;
}
&::after {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
}
}
}
.mx_SearchBox {
margin: 0 0 20px;
}
.mx_SpaceFeedbackPrompt {
margin-bottom: 16px;
// hide the HR as we have our own
& + hr {
display: none;
}
}
}
.mx_SpaceRoomView_privateScope {
.mx_AccessibleButton {
> .mx_AccessibleButton {
@mixin SpacePillButton;
}
@ -362,6 +365,23 @@ $SpaceRoomViewInnerWidth: 428px;
}
.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 {
color: $secondary-fg-color;
margin-top: 28px;
@ -443,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;
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;
}
@ -75,10 +76,124 @@ limitations under the License.
}
.mx_AddExistingToSpace_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image {
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 {
@ -101,7 +216,7 @@ limitations under the License.
.mx_BaseAvatar {
display: inline-flex;
margin: 5px 16px 5px 5px;
margin: auto 16px auto 5px;
vertical-align: middle;
}
@ -160,44 +275,7 @@ limitations under the License.
}
}
.mx_AddExistingToSpaceDialog_errorText {
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
color: $notice-primary-color;
margin-bottom: 28px;
}
.mx_AddExistingToSpace {
display: contents;
}
.mx_AddExistingToSpaceDialog_footer {
display: flex;
margin-top: 32px;
> span {
flex-grow: 1;
font-size: $font-14px;
line-height: $font-15px;
font-weight: $font-semi-bold;
.mx_AccessibleButton {
font-size: inherit;
display: inline-block;
}
> * {
vertical-align: middle;
}
}
.mx_AccessibleButton {
display: inline-block;
}
.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

@ -76,12 +76,16 @@ limitations under the License.
border: 1px solid $button-danger-bg-color;
}
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
color: $button-danger-disabled-bg-color;
border-color: $button-danger-disabled-bg-color;
}
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
padding: 5px 12px;
color: $button-danger-fg-color;

View File

@ -18,7 +18,11 @@ limitations under the License.
display: inline;
}
.mx_InlineSpinner_spin img {
.mx_InlineSpinner img, .mx_InlineSpinner_icon {
margin: 0px 6px;
vertical-align: -3px;
}
.mx_InlineSpinner_icon {
display: inline-block;
}

View File

@ -28,8 +28,7 @@ limitations under the License.
top: 0;
}
&::before, &::after {
content: '';
.mx_MiniAvatarUploader_indicator {
position: absolute;
height: 26px;
@ -37,27 +36,22 @@ limitations under the License.
right: -6px;
bottom: -6px;
}
&::before {
background-color: $primary-bg-color;
border-radius: 50%;
z-index: 1;
}
&::after {
background-color: $secondary-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/camera.svg');
mask-size: 16px;
z-index: 2;
}
.mx_MiniAvatarUploader_cameraIcon {
height: 100%;
width: 100%;
&.mx_MiniAvatarUploader_busy::after {
background: url("$(res)/img/spinner.gif") no-repeat center;
background-size: 80%;
mask: unset;
background-color: $secondary-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/camera.svg');
mask-size: 16px;
z-index: 2;
}
}
}

View File

@ -21,7 +21,7 @@ progress.mx_ProgressBar {
appearance: none;
border: none;
@mixin ProgressBarBorderRadius "6px";
@mixin ProgressBarBorderRadius 6px;
@mixin ProgressBarColour $progressbar-fg-color;
@mixin ProgressBarBgColour $progressbar-bg-color;
::-webkit-progress-value {

View File

@ -26,3 +26,19 @@ limitations under the License.
.mx_MatrixChat_middlePanel .mx_Spinner {
height: auto;
}
@keyframes spin {
from {
transform: rotateZ(0deg);
}
to {
transform: rotateZ(360deg);
}
}
.mx_Spinner_icon {
background-color: $primary-fg-color;
mask: url('$(res)/img/spinner.svg');
mask-size: contain;
animation: 1.1s steps(12, end) infinite spin;
}

View File

@ -61,9 +61,9 @@ limitations under the License.
.mx_MFileBody_info {
background-color: $message-body-panel-bg-color;
border-radius: 4px;
width: 270px;
padding: 8px;
border-radius: 12px;
width: 243px; // same width as a playable voice message, accounting for padding
padding: 6px 12px;
color: $message-body-panel-fg-color;
.mx_MFileBody_info_icon {
@ -82,7 +82,7 @@ limitations under the License.
mask-position: center;
mask-size: cover;
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
background-color: $message-body-panel-fg-color;
background-color: $message-body-panel-icon-fg-color;
width: 13px;
height: 15px;

View File

@ -0,0 +1,19 @@
/*
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_MVoiceMessageBody {
display: inline-block; // makes the playback controls magically line up
}

View File

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

View File

@ -17,18 +17,56 @@ limitations under the License.
.mx_ReactionsRow {
margin: 6px 0;
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 {
text-decoration: none;
font-size: $font-10px;
font-weight: 600;
margin-left: 6px;
vertical-align: top;
font-size: $font-12px;
line-height: $font-20px;
margin-left: 4px;
vertical-align: middle;
&:hover,
&:link,
&:visited {
color: $accent-color;
&:link, &:visited {
color: $tertiary-fg-color;
}
&:hover {
color: $primary-fg-color;
}
}

View File

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

View File

@ -36,6 +36,7 @@ limitations under the License.
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
}
.mx_RoomSummaryCard_avatar {

View File

@ -18,8 +18,8 @@ limitations under the License.
margin: 40px 0 48px 64px;
.mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
&::before, &::after {
content: unset;
.mx_MiniAvatarUploader_indicator {
display: none;
}
}

View File

@ -98,7 +98,7 @@ limitations under the License.
position: relative;
width: 24px;
height: 24px;
border-radius: 32px;
border-radius: 8px;
&::before {
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
.mx_RoomSublist_menuButton {
visibility: hidden;

View File

@ -39,23 +39,25 @@ limitations under the License.
width: 14px; // w&h are size of icon
height: 18px;
vertical-align: middle;
margin-right: 7px; // distance from left edge of waveform container (container has some margin too)
background-color: $muted-fg-color;
margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
background-color: $voice-record-icon-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/trashcan.svg');
}
.mx_VoiceMessagePrimaryContainer {
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
// Note: remaining class properties are in the PlayerContainer CSS.
margin: 6px; // force the composer area to put a gutter around us
margin-right: 12px; // isolate from stop button
margin-right: 12px; // isolate from stop/send button
position: relative; // important for the live circle
&.mx_VoiceRecordComposerTile_recording {
padding-left: 16px; // +10px for the live circle, +6px for regular padding
// We are putting the circle in this padding, so we need +10px from the regular
// padding on the left side.
padding-left: 22px;
&::before {
animation: recording-pulse 2s infinite;
@ -65,8 +67,8 @@ limitations under the License.
width: 10px;
height: 10px;
position: absolute;
left: 8px;
top: 16px; // vertically center
left: 12px; // 12px from the left edge for container padding
top: 18px; // vertically center (middle align with clock)
border-radius: 10px;
}
}

View File

@ -22,3 +22,34 @@ limitations under the License.
.mx_HelpUserSettingsTab span.mx_AccessibleButton {
word-break: break-word;
}
.mx_HelpUserSettingsTab code {
word-break: break-all;
user-select: all;
}
.mx_HelpUserSettingsTab_accessToken {
display: flex;
justify-content: space-between;
border-radius: 5px;
border: solid 1px $light-fg-color;
margin-bottom: 10px;
margin-top: 10px;
padding: 10px;
}
.mx_HelpUserSettingsTab_accessToken_copy {
flex-shrink: 0;
cursor: pointer;
margin-left: 20px;
display: inherit;
}
.mx_HelpUserSettingsTab_accessToken_copy > div {
mask-image: url($copy-button-url);
background-color: $message-action-bar-fg-color;
margin-left: 5px;
width: 20px;
height: 20px;
background-repeat: no-repeat;
}

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;
box-sizing: border-box;
background-color: $primary-bg-color;
position: relative;
> div {
> 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 {
@mixin SpacePillButton;
}
@ -59,7 +67,7 @@ $spacePanelWidth: 71px;
width: 28px;
height: 28px;
position: relative;
background-color: $theme-button-bg-color;
background-color: $roomlist-button-bg-color;
border-radius: 14px;
margin-bottom: 12px;
@ -70,7 +78,7 @@ $spacePanelWidth: 71px;
width: 28px;
top: 0;
left: 0;
background-color: $muted-fg-color;
background-color: $tertiary-fg-color;
transform: rotate(90deg);
mask-repeat: no-repeat;
mask-position: 2px 3px;

View File

@ -19,12 +19,12 @@ limitations under the License.
width: 32px;
height: 32px;
border-radius: 32px;
background-color: $primary-bg-color;
background-color: $voice-playback-button-bg-color;
&::before {
content: '';
position: absolute; // sizing varies by icon
background-color: $muted-fg-color;
background-color: $voice-playback-button-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
}

View File

@ -19,8 +19,9 @@ limitations under the License.
// Container for live recording and playback controls
.mx_VoiceMessagePrimaryContainer {
padding: 6px; // makes us 4px taller than the send/stop button
padding-right: 5px; // there's 1px from the waveform itself, so account for that
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
// has a 1px padding on it that we want to account for.
padding: 7px 12px 7px 11px;
background-color: $voice-record-waveform-bg-color;
border-radius: 12px;
@ -30,11 +31,9 @@ limitations under the License.
color: $voice-record-waveform-fg-color;
font-size: $font-14px;
line-height: $font-24px;
.mx_Waveform {
// We want the bars to be 2px shorter than the play/pause button in the waveform control
height: 28px; // default is 30px, so we're subtracting the 2px border off the bars
.mx_Waveform_bar {
background-color: $voice-record-waveform-incomplete-fg-color;
@ -47,8 +46,8 @@ limitations under the License.
}
.mx_Clock {
padding-right: 4px; // isolate from waveform
padding-left: 8px; // isolate from live circle
width: 40px; // 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-left: 8px; // isolate from recording circle / play control
}
}

View File

@ -65,14 +65,17 @@ limitations under the License.
}
}
.mx_CallView_voice {
.mx_CallView_content {
position: relative;
display: flex;
flex-direction: column;
border-radius: 8px;
}
.mx_CallView_voice {
align-items: center;
justify-content: center;
flex-direction: column;
background-color: $inverted-bg-color;
border-radius: 8px;
}
.mx_CallView_voice_avatarsContainer {
@ -109,9 +112,7 @@ limitations under the License.
.mx_CallView_video {
width: 100%;
height: 100%;
position: relative;
z-index: 30;
border-radius: 8px;
overflow: hidden;
}

View File

@ -14,21 +14,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_VideoFeed_voice {
// We don't want to collide with the call controls that have 52px of height
padding-bottom: 52px;
background-color: $inverted-bg-color;
}
.mx_VideoFeed_remote {
width: 100%;
height: 100%;
background-color: #000;
z-index: 50;
display: flex;
justify-content: center;
align-items: center;
&.mx_VideoFeed_video {
background-color: #000;
}
}
.mx_VideoFeed_local {
width: 25%;
height: 25%;
max-width: 25%;
max-height: 25%;
position: absolute;
right: 10px;
top: 10px;
z-index: 100;
border-radius: 4px;
&.mx_VideoFeed_video {
background-color: transparent;
}
}
.mx_VideoFeed_mirror {

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">
<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"/>
</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 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)"/>
<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 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>
<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="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>

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">
<ellipse cx="5" cy="5.75" rx="1.5" ry="1.75" fill="black"/>
<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 width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,141 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink" preserveAspectRatio="none" viewBox="0 0 375 375" style="background-color:#FFFFFF00; overflow:visible">
<title>start</title>
<!-- Layers -->
<!-- Layer: Icon -->
<svg x="188" y="187" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="188;187.75;187.5"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="187;187.25;187.5"/>
<g transform="scale(1 1)">
<g transform="rotate(0)">
<animateTransform attributeName="transform" calcMode="spline" dur="2" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1"
repeatCount="indefinite" type="rotate" values="0;180;360"/>
<svg x="-100" y="-100" width="200" height="200" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
<g clip-path="">
<g filter="">
<!-- Layer: 1024@2x -->
<svg x="100" y="100" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="100;117.5;100"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="100;117.5;100"/>
<g transform="scale(1 1)">
<g transform="rotate(0)">
<svg x="-100" y="-100" width="200" height="200" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
<g clip-path="">
<g filter="">
<!-- Layer: Path -->
<svg x="118" y="46" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="118;138.65;118"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="46;54.05;46"/>
<g transform="scale(1 1)">
<g transform="rotate(0)">
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<g clip-path="">
<g filter="">
<path d="M0,12c0,-6.627,5.373,-12,12,-12 44.183,0,80,35.817,80,80 0,6.627,-5.373,12,-12,12 -6.627,0,-12,-5.373,-12,-12 0,-30.928,-25.072,-56,-56,-56 -6.627,0,-12,-5.373,-12,-12zM0,12" fill="#0DBD8B" id="path" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M0,12c0,-6.627,5.373,-12,12,-12 44.183,0,80,35.817,80,80 0,6.627,-5.373,12,-12,12 -6.627,0,-12,-5.373,-12,-12 0,-30.928,-25.072,-56,-56,-56 -6.627,0,-12,-5.373,-12,-12zM0,12;M0,14.1c0,-7.787,6.313,-14.1,14.1,-14.1 51.915,0,94,42.085,94,94 0,7.787,-6.313,14.1,-14.1,14.1 -7.787,0,-14.1,-6.313,-14.1,-14.1 0,-36.34,-29.46,-65.8,-65.8,-65.8 -7.787,0,-14.1,-6.313,-14.1,-14.1zM0,14.1;M0,12c0,-6.627,5.373,-12,12,-12 44.183,0,80,35.817,80,80 0,6.627,-5.373,12,-12,12 -6.627,0,-12,-5.373,-12,-12 0,-30.928,-25.072,-56,-56,-56 -6.627,0,-12,-5.373,-12,-12zM0,12"/>
</path>
</g>
</g>
</svg>
</g>
</g>
</svg>
<!-- Layer: Path -->
<svg x="82" y="154" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="82;96.35;82"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="154;180.95;154"/>
<g transform="scale(1 1)">
<g transform="rotate(0)">
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<g clip-path="">
<g filter="">
<path d="M92,80c0,6.627,-5.373,12,-12,12 -44.183,0,-80,-35.817,-80,-80 0,-6.627,5.373,-12,12,-12 6.627,0,12,5.373,12,12 0,30.928,25.072,56,56,56 6.627,0,12,5.373,12,12zM92,80" fill="#0DBD8B" id="path_1" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path_1" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M92,80c0,6.627,-5.373,12,-12,12 -44.183,0,-80,-35.817,-80,-80 0,-6.627,5.373,-12,12,-12 6.627,0,12,5.373,12,12 0,30.928,25.072,56,56,56 6.627,0,12,5.373,12,12zM92,80;M108.1,94c0,7.787,-6.313,14.1,-14.1,14.1 -51.915,0,-94,-42.085,-94,-94 0,-7.787,6.313,-14.1,14.1,-14.1 7.787,0,14.1,6.313,14.1,14.1 0,36.34,29.46,65.8,65.8,65.8 7.787,0,14.1,6.313,14.1,14.1zM108.1,94;M92,80c0,6.627,-5.373,12,-12,12 -44.183,0,-80,-35.817,-80,-80 0,-6.627,5.373,-12,12,-12 6.627,0,12,5.373,12,12 0,30.928,25.072,56,56,56 6.627,0,12,5.373,12,12zM92,80"/>
</path>
</g>
</g>
</svg>
</g>
</g>
</svg>
<!-- Layer: Path -->
<svg x="46" y="82" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="46;54.05;46"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="82;96.35;82"/>
<g transform="scale(1 1)">
<g transform="rotate(0)">
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<g clip-path="">
<g filter="">
<path d="M12,92c-6.627,0,-12,-5.373,-12,-12 0,-44.183,35.817,-80,80,-80 6.627,0,12,5.373,12,12 0,6.627,-5.373,12,-12,12 -30.928,0,-56,25.072,-56,56 0,6.627,-5.373,12,-12,12zM12,92" fill="#0DBD8B" id="path_2" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path_2" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M12,92c-6.627,0,-12,-5.373,-12,-12 0,-44.183,35.817,-80,80,-80 6.627,0,12,5.373,12,12 0,6.627,-5.373,12,-12,12 -30.928,0,-56,25.072,-56,56 0,6.627,-5.373,12,-12,12zM12,92;M14.1,108.1c-7.787,0,-14.1,-6.313,-14.1,-14.1 0,-51.915,42.085,-94,94,-94 7.787,0,14.1,6.313,14.1,14.1 0,7.787,-6.313,14.1,-14.1,14.1 -36.34,0,-65.8,29.46,-65.8,65.8 0,7.787,-6.313,14.1,-14.1,14.1zM14.1,108.1;M12,92c-6.627,0,-12,-5.373,-12,-12 0,-44.183,35.817,-80,80,-80 6.627,0,12,5.373,12,12 0,6.627,-5.373,12,-12,12 -30.928,0,-56,25.072,-56,56 0,6.627,-5.373,12,-12,12zM12,92"/>
</path>
</g>
</g>
</svg>
</g>
</g>
</svg>
<!-- Layer: Path -->
<svg x="154" y="118" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="154;180.95;154"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="118;138.65;118"/>
<g transform="scale(1 1)">
<g transform="rotate(0)">
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
<g clip-path="">
<g filter="">
<path d="M80,0c6.627,0,12,5.373,12,12 0,44.183,-35.817,80,-80,80 -6.627,0,-12,-5.373,-12,-12 0,-6.627,5.373,-12,12,-12 30.928,0,56,-25.072,56,-56 0,-6.627,5.373,-12,12,-12zM80,0" fill="#0DBD8B" id="path_3" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path_3" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M80,0c6.627,0,12,5.373,12,12 0,44.183,-35.817,80,-80,80 -6.627,0,-12,-5.373,-12,-12 0,-6.627,5.373,-12,12,-12 30.928,0,56,-25.072,56,-56 0,-6.627,5.373,-12,12,-12zM80,0;M94,0c7.787,0,14.1,6.313,14.1,14.1 0,51.915,-42.085,94,-94,94 -7.787,0,-14.1,-6.313,-14.1,-14.1 0,-7.787,6.313,-14.1,14.1,-14.1 36.34,0,65.8,-29.46,65.8,-65.8 0,-7.787,6.313,-14.1,14.1,-14.1zM94,0;M80,0c6.627,0,12,5.373,12,12 0,44.183,-35.817,80,-80,80 -6.627,0,-12,-5.373,-12,-12 0,-6.627,5.373,-12,12,-12 30.928,0,56,-25.072,56,-56 0,-6.627,5.373,-12,12,-12zM80,0"/>
</path>
</g>
</g>
</svg>
</g>
</g>
</svg>
</g>
</g>
</svg>
</g>
</g>
</svg>
</g>
</g>
</svg>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg920"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="spinner.svg">
<defs
id="defs914" />
<metadata
id="metadata917">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="M 59,95.605469 V 123 c 0,2.77 2.23,5 5,5 2.77,0 5,-2.23 5,-5 V 95.605469 A 31.999998,31.999998 0 0 1 64,96 31.999998,31.999998 0 0 1 59,95.605469 Z"
transform="scale(0.26458333)"
id="path2350" />
<path
style="stroke-width:0;fill-opacity:0.7020452"
d="M 64,0 C 61.23,0 59,2.2300001 59,5 V 32.394531 A 31.999998,31.999998 0 0 1 64,32 31.999998,31.999998 0 0 1 69,32.394531 V 5 C 69,2.2300001 66.77,0 64,0 Z"
transform="scale(0.26458333)"
id="rect2283" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="M 43.867188,88.871094 30.169922,112.5957 c -1.385,2.39889 -0.568812,5.44508 1.830078,6.83008 2.39889,1.385 5.445078,0.56881 6.830078,-1.83008 L 52.527344,93.873047 a 31.999998,31.999998 0 0 1 -8.660156,-5.001953 z"
transform="scale(0.26458333)"
id="path2346" />
<path
style="stroke-width:0;fill-opacity:0.80019373"
d="m 93.150391,7.9121094 c -1.599848,0.111837 -3.114844,0.992881 -3.980469,2.4921876 L 75.472656,34.126953 a 31.999998,31.999998 0 0 1 8.660156,5.001953 L 97.830078,15.404297 C 99.215078,13.005407 98.39889,9.9592187 96,8.5742188 95.100416,8.0548438 94.110299,7.8450072 93.150391,7.9121094 Z"
transform="scale(0.26458333)"
id="rect2285" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="M 34.126953,75.474609 10.404297,89.169922 C 8.0054066,90.554922 7.1892188,93.60111 8.5742188,96 c 1.3849999,2.39889 4.4311882,3.215078 6.8300782,1.830078 L 39.128906,84.132812 a 31.999998,31.999998 0 0 1 -5.001953,-8.658203 z"
transform="scale(0.26458333)"
id="path2342" />
<path
style="stroke-width:0;fill-opacity:0.90226436"
d="m 115.44531,29.507812 c -0.95991,-0.0671 -1.95002,0.142735 -2.84961,0.66211 L 88.871094,43.867188 a 31.999998,31.999998 0 0 1 5.001953,8.658203 L 117.5957,38.830078 c 2.39889,-1.385 3.21508,-4.431188 1.83008,-6.830078 -0.86562,-1.499306 -2.38062,-2.38035 -3.98047,-2.492188 z"
transform="scale(0.26458333)"
id="rect2287" />
<path
style="stroke-width:0;fill-opacity:1"
d="M 95.605469,59 A 31.999998,31.999998 0 0 1 96,64 31.999998,31.999998 0 0 1 95.605469,69 H 123 c 2.77,0 5,-2.23 5,-5 0,-2.77 -2.23,-5 -5,-5 z"
transform="scale(0.26458333)"
id="path2338" />
<path
style="stroke-width:0;fill-opacity:0.40288368"
d="m 5,59 c -2.7699999,0 -5,2.23 -5,5 0,2.77 2.2300001,5 5,5 H 32.394531 A 31.999998,31.999998 0 0 1 32,64 31.999998,31.999998 0 0 1 32.394531,59 Z"
transform="scale(0.26458333)"
id="rect2289" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="m 93.873047,75.472656 a 31.999998,31.999998 0 0 1 -5.001953,8.660156 L 112.5957,97.830078 c 2.39889,1.385 5.44508,0.568812 6.83008,-1.830078 1.385,-2.39889 0.56881,-5.445078 -1.83008,-6.830078 z"
transform="scale(0.26458333)"
id="path2334" />
<path
style="stroke-width:0;fill-opacity:0.49898377"
d="M 12.554688,29.507812 C 10.95484,29.61965 9.4398437,30.500694 8.5742188,32 c -1.385,2.39889 -0.5688122,5.445078 1.8300782,6.830078 l 23.722656,13.697266 a 31.999998,31.999998 0 0 1 5.001953,-8.660156 L 15.404297,30.169922 c -0.899584,-0.519375 -1.889701,-0.729212 -2.849609,-0.66211 z"
transform="scale(0.26458333)"
id="rect2291" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="m 84.132812,88.871094 a 31.999998,31.999998 0 0 1 -8.658203,5.001953 L 89.169922,117.5957 c 1.385,2.39889 4.431188,3.21508 6.830078,1.83008 2.39889,-1.385 3.215078,-4.43119 1.830078,-6.83008 z"
transform="scale(0.26458333)"
id="path2330" />
<path
style="stroke-width:0;fill-opacity:0.5998317"
d="M 34.849609,7.9121094 C 33.889701,7.8450072 32.899584,8.0548438 32,8.5742188 29.60111,9.9592187 28.784922,13.005407 30.169922,15.404297 l 13.697266,23.724609 a 31.999998,31.999998 0 0 1 8.658203,-5.001953 L 38.830078,10.404297 C 37.964453,8.9049904 36.449457,8.0239464 34.849609,7.9121094 Z"
transform="scale(0.26458333)"
id="rect2293" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6;
$header-panel-text-secondary-color: #c8c8cd;
$text-primary-color: #ffffff;
$text-secondary-color: #B9BEC6;
$quaternary-fg-color: #6F7882;
$search-bg-color: #181b21;
$search-placeholder-color: #61708b;
$room-highlight-color: #343a46;
@ -205,9 +206,18 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #21262c82;
$message-body-panel-icon-bg-color: #8e99a4;
$message-body-panel-fg-color: $primary-fg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049; // "Dark Tile"
$message-body-panel-icon-fg-color: #21262C; // "Separator"
$message-body-panel-icon-bg-color: $tertiary-fg-color;
$voice-record-stop-border-color: $quaternary-fg-color;
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
$voice-record-icon-color: $quaternary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
// Appearance tab colors
$appearance-tab-border-color: $room-highlight-color;

View File

@ -200,9 +200,19 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #21262c82;
$message-body-panel-icon-bg-color: #8e99a4;
$message-body-panel-fg-color: $primary-fg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049;
$message-body-panel-icon-fg-color: $primary-bg-color;
$message-body-panel-icon-bg-color: $secondary-fg-color;
// See non-legacy dark for variable information
$voice-record-stop-border-color: #6F7882;
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: #6F7882;
$voice-record-icon-color: #6F7882;
$voice-playback-button-bg-color: $tertiary-fg-color;
$voice-playback-button-fg-color: #21262C;
// Appearance tab colors
$appearance-tab-border-color: $room-highlight-color;

View File

@ -191,14 +191,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
// See non-legacy _light for variable information
$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: #ff4b55;
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-live-circle-color: #ff4b55;
$roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #fff;
@ -331,9 +323,21 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #e3e8f082;
$message-body-panel-icon-bg-color: #ffffff;
$message-body-panel-fg-color: $muted-fg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0;
$message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $primary-bg-color;
// See non-legacy _light for variable information
$voice-record-stop-symbol-color: #ff4b55;
$voice-record-live-circle-color: #ff4b55;
$voice-record-stop-border-color: #E3E8F0;
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
// FontSlider colors
$appearance-tab-border-color: $input-darker-bg-color;

View File

@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
$primary-fg-color: #2e2f32;
$secondary-fg-color: #737D8C;
$tertiary-fg-color: #8D99A5;
$quaternary-fg-color: #C1C6CD;
$header-panel-bg-color: #f3f8fd;
// typical text (dark-on-white in light skin)
@ -182,13 +183,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes
$roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #FFF;
@ -328,9 +322,23 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #e3e8f082;
$message-body-panel-icon-bg-color: #ffffff;
$message-body-panel-fg-color: $muted-fg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0; // "Separator"
$message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $primary-bg-color;
// These two don't change between themes. They are the $warning-color, but we don't
// want custom themes to affect them by accident.
$voice-record-stop-symbol-color: #ff4b55;
$voice-record-live-circle-color: #ff4b55;
$voice-record-stop-border-color: #E3E8F0; // "Separator"
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
$voice-record-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
// FontSlider colors
$appearance-tab-border-color: $input-darker-bg-color;

View File

@ -42,6 +42,7 @@ import {SpaceStoreClass} from "../stores/SpaceStore";
import TypingStore from "../stores/TypingStore";
import { EventIndexPeg } from "../indexing/EventIndexPeg";
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
import PerformanceMonitor from "../performance";
declare global {
interface Window {
@ -52,6 +53,9 @@ declare global {
init: () => Promise<void>;
};
// Needed for Safari, unknown to TypeScript
webkitAudioContext: typeof AudioContext;
mxContentMessages: ContentMessages;
mxToastStore: ToastStore;
mxDeviceListener: DeviceListener;
@ -76,6 +80,8 @@ declare global {
mxVoiceRecordingStore: VoiceRecordingStore;
mxTypingStore: TypingStore;
mxEventIndexPeg: EventIndexPeg;
mxPerformanceMonitor: PerformanceMonitor;
mxPerformanceEntryNames: any;
}
interface Document {
@ -118,6 +124,16 @@ declare global {
interface HTMLAudioElement {
type?: string;
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
setSinkId(outputId: string);
}
interface HTMLVideoElement {
type?: string;
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
setSinkId(outputId: string);
}
interface Element {

View File

@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
import DMRoomMap from './utils/DMRoomMap';
import {mediaFromMxc} from "./customisations/Media";
import SettingsStore from "./settings/SettingsStore";
export type ResizeMethod = "crop" | "scale";
@ -143,7 +144,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
}
// space rooms cannot be DMs so skip the rest
if (room.isSpaceRoom()) return null;
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null;
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);

View File

@ -85,6 +85,7 @@ import { Action } from './dispatcher/actions';
import VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
import EventEmitter from 'events';
import SdkConfig from './SdkConfig';
import { ensureDMExists, findDMForUser } from './createRoom';
@ -138,22 +139,12 @@ export enum PlaceCallType {
ScreenSharing = 'screensharing',
}
function getRemoteAudioElement(): HTMLAudioElement {
// this needs to be somewhere at the top of the DOM which
// always exists to avoid audio interruptions.
// Might as well just use DOM.
const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
if (!remoteAudioElement) {
console.error(
"Failed to find remoteAudio element - cannot play audio!" +
"You need to add an <audio/> to the DOM.",
);
return null;
}
return remoteAudioElement;
export enum CallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
}
export default class CallHandler {
export default class CallHandler extends EventEmitter {
private calls = new Map<string, MatrixCall>(); // roomId -> call
// Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one.
@ -514,6 +505,7 @@ export default class CallHandler {
}
this.calls.set(mappedRoomId, newCall);
this.emit(CallHandlerEvent.CallsChanged, this.calls);
this.setCallListeners(newCall);
this.setCallState(newCall, newCall.state);
});
@ -546,10 +538,7 @@ export default class CallHandler {
this.removeCallForRoom(mappedRoomId);
mappedRoomId = newMappedRoomId;
this.calls.set(mappedRoomId, call);
dis.dispatch({
action: Action.CallChangeRoom,
call,
});
this.emit(CallHandlerEvent.CallChangeRoom, call);
}
}
});
@ -598,11 +587,6 @@ export default class CallHandler {
}
}
private setCallAudioElement(call: MatrixCall) {
const audioElement = getRemoteAudioElement();
if (audioElement) call.setRemoteAudioElement(audioElement);
}
private setCallState(call: MatrixCall, status: CallState) {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
@ -619,6 +603,7 @@ export default class CallHandler {
private removeCallForRoom(roomId: string) {
this.calls.delete(roomId);
this.emit(CallHandlerEvent.CallsChanged, this.calls);
}
private showICEFallbackPrompt() {
@ -679,11 +664,7 @@ export default class CallHandler {
}, null, true);
}
private async placeCall(
roomId: string, type: PlaceCallType,
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
transferee: MatrixCall,
) {
private async placeCall(roomId: string, type: PlaceCallType, transferee: MatrixCall) {
Analytics.trackEvent('voip', 'placeCall', 'type', type);
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
@ -695,22 +676,19 @@ export default class CallHandler {
const call = MatrixClientPeg.get().createCall(mappedRoomId);
this.calls.set(roomId, call);
this.emit(CallHandlerEvent.CallsChanged, this.calls);
if (transferee) {
this.transferees[call.callId] = transferee;
}
this.setCallListeners(call);
this.setCallAudioElement(call);
this.setActiveCallRoomId(roomId);
if (type === PlaceCallType.Voice) {
call.placeVoiceCall();
} else if (type === 'video') {
call.placeVideoCall(
remoteElement,
localElement,
);
call.placeVideoCall();
} else if (type === PlaceCallType.ScreenSharing) {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) {
@ -724,13 +702,12 @@ export default class CallHandler {
}
call.placeScreenSharingCall(
remoteElement,
localElement,
async (): Promise<DesktopCapturerSource> => {
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
return source;
});
},
);
} else {
console.error("Unknown conf call type: " + type);
}
@ -787,17 +764,12 @@ export default class CallHandler {
} else if (members.length === 2) {
console.info(`Place ${payload.type} call in ${payload.room_id}`);
this.placeCall(
payload.room_id, payload.type, payload.local_element, payload.remote_element,
payload.transferee,
);
this.placeCall(payload.room_id, payload.type, payload.transferee);
} else { // > 2
dis.dispatch({
action: "place_conference_call",
room_id: payload.room_id,
type: payload.type,
remote_element: payload.remote_element,
local_element: payload.local_element,
});
}
}
@ -833,6 +805,7 @@ export default class CallHandler {
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
this.calls.set(mappedRoomId, call)
this.emit(CallHandlerEvent.CallsChanged, this.calls);
this.setCallListeners(call);
// get ready to send encrypted events in the room, so if the user does answer
@ -875,7 +848,6 @@ export default class CallHandler {
const call = this.calls.get(payload.room_id);
call.answer();
this.setCallAudioElement(call);
this.setActiveCallRoomId(payload.room_id);
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
dis.dispatch({

View File

@ -16,7 +16,7 @@
import SettingsStore from "./settings/SettingsStore";
import {SettingLevel} from "./settings/SettingLevel";
import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
export default {
hasAnyLabeledDevices: async function() {
@ -50,18 +50,15 @@ export default {
},
loadDevices: function() {
const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
setMatrixCallAudioOutput(audioOutDeviceId);
setMatrixCallAudioInput(audioDeviceId);
setMatrixCallVideoInput(videoDeviceId);
},
setAudioOutput: function(deviceId) {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallAudioOutput(deviceId);
},
setAudioInput: function(deviceId) {

View File

@ -148,13 +148,15 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog(
'Failed to add the following room to the group',
'', ErrorDialog,
{
title: _t(
"Failed to add the following rooms to %(groupId)s:",
{groupId},
),
description: errorList.join(", "),
});
'',
ErrorDialog,
{
title: _t(
"Failed to add the following rooms to %(groupId)s:",
{groupId},
),
description: errorList.join(", "),
},
);
});
}

View File

@ -422,8 +422,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
if (SettingsStore.getValue("feature_latex_maths")) {
const phtml = cheerio.load(safeBody,
{ _useHtmlParser2: true, decodeEntities: false })
const phtml = cheerio.load(safeBody, {
// @ts-ignore: The `_useHtmlParser2` internal option is the
// simplest way to both parse and render using `htmlparser2`.
_useHtmlParser2: true,
decodeEntities: false,
});
// @ts-ignore - The types for `replaceWith` wrongly expect
// Cheerio instance to be returned.
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
@ -431,6 +435,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
{
throwOnError: false,
// @ts-ignore - `e` can be an Element, not just a Node
displayMode: e.name == 'div',
output: "htmlAndMathml",
});

View File

@ -163,7 +163,7 @@ export default class IdentityAuthClient {
</div>
),
button: _t("Trust"),
});
});
const [confirmed] = await finished;
if (confirmed) {
// eslint-disable-next-line react-hooks/rules-of-hooks

View File

@ -331,6 +331,8 @@ export const Notifier = {
if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
MatrixClientPeg.get().decryptEventIfNeeded(ev);
// If it's an encrypted event and the type is still 'm.room.encrypted',
// it hasn't yet been decrypted, so wait until it is.
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) {

View File

@ -54,7 +54,7 @@ export default class PasswordReset {
return res;
}, function(err) {
if (err.errcode === 'M_THREEPID_NOT_FOUND') {
err.message = _t('This email address was not found');
err.message = _t('This email address was not found');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}

View File

@ -38,7 +38,7 @@ import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks";
import {inviteUsersToRoom} from "./RoomInvite";
import { WidgetType } from "./widgets/WidgetType";
import { Jitsi } from "./widgets/Jitsi";
import { parseFragment as parseHtml } from "parse5";
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
import BugReportDialog from "./components/views/dialogs/BugReportDialog";
import { ensureDMExists } from "./createRoom";
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
@ -856,7 +856,7 @@ export const Commands = [
// some superfast regex over the text so we don't have to.
const embed = parseHtml(widgetUrl);
if (embed && embed.childNodes && embed.childNodes.length === 1) {
const iframe = embed.childNodes[0];
const iframe = embed.childNodes[0] as ChildElement;
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
const srcAttr = iframe.attrs.find(a => a.name === 'src');
console.log("Pulling URL out of iframe (embed code)");

View File

@ -547,17 +547,23 @@ function textForMjolnirEvent(event) {
// else the entity !== prevEntity - count as a removal & add
if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
return _t(
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
);
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
return _t(
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
);
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
return _t(
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
);
}
// Unknown type. We'll say something but we shouldn't end up here.

View File

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

View File

@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
const onKeyDownHandler = useCallback((ev) => {
let handled = false;
// 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
switch (ev.key) {
case Key.HOME:

View File

@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Please enter your Security Phrase a second time to confirm.",
"Enter your Security Phrase a second time to confirm it.",
)}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -498,9 +498,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
title={this._titleForPhase(this.state.phase)}
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
>
<div>
{content}
</div>
<div>
{content}
</div>
</BaseDialog>
);
}

View File

@ -647,7 +647,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Enter your recovery passphrase a second time to confirm it.",
"Enter your Security Phrase a second time to confirm it.",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
@ -655,7 +655,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your recovery passphrase")}
label={_t("Confirm your Security Phrase")}
autoFocus={true}
autoComplete="new-password"
/>
@ -856,9 +856,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
fixedWidth={false}
>
<div>
{content}
</div>
<div>
{content}
</div>
</BaseDialog>
);
}

View File

@ -170,8 +170,11 @@ export default class ExportE2eKeysDialog extends React.Component {
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value={_t('Export')}
disabled={disableForm}
<input
className='mx_Dialog_primary'
type='submit'
value={_t('Export')}
disabled={disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
{ _t("Cancel") }

View File

@ -140,36 +140,36 @@ export default class ImportE2eKeysDialog extends React.Component {
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'>
{ _t("File to import") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this._file}
id='importFile'
type='file'
autoFocus={true}
onChange={this._onFormChange}
disabled={disableForm} />
</div>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'>
{ _t("File to import") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this._file}
id='importFile'
type='file'
autoFocus={true}
onChange={this._onFormChange}
disabled={disableForm} />
</div>
</div>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'>
{ _t("Enter passphrase") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this._passphrase}
id='passphrase'
size='64'
type='password'
onChange={this._onFormChange}
disabled={disableForm} />
</div>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'>
{ _t("Enter passphrase") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this._passphrase}
id='passphrase'
size='64'
type='password'
onChange={this._onFormChange}
disabled={disableForm} />
</div>
</div>
</div>
</div>

View File

@ -93,7 +93,12 @@ export default class AutocompleteProvider {
};
}
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
return [];
}

View File

@ -26,6 +26,8 @@ import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider';
import {timeout} from "../utils/promise";
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
import SettingsStore from "../settings/SettingsStore";
import SpaceProvider from "./SpaceProvider";
export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not
@ -56,6 +58,11 @@ const PROVIDERS = [
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.
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,
otherwise, we run into a condition where new completions are displayed
while the user is interacting with the list, which makes it difficult
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
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(provider => {
return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT);
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => {
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

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);
if (!command) return [];
@ -55,10 +60,11 @@ export default class CommandProvider extends AutocompleteProvider {
} else {
if (query === '/') {
// If they have just entered `/` show everything
// We exclude the limit on purpose to have a comprehensive list
matches = Commands;
} else {
// 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');
// Disable autocompletions when composing commands because of various issues
@ -81,7 +86,7 @@ export default class CommunityProvider extends AutocompleteProvider {
this.matcher.setObjects(groups);
const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [
(c) => score(matchedString, c.groupId),
(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)}`;
}
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);
if (!query || !command) {
return [];
@ -46,7 +51,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
method: 'GET',
});
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 {
completion: result.Text,
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")) {
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);
if (command) {
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'
completions = completions.concat(this.nameMatcher.match(matchedString));

View File

@ -33,7 +33,12 @@ export default class NotifProvider extends AutocompleteProvider {
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 client = MatrixClientPeg.get();

View File

@ -21,7 +21,7 @@ import {removeHiddenChars} from "matrix-js-sdk/src/utils";
interface IOptions<T extends {}> {
keys: Array<string | keyof T>;
funcs?: Array<(T) => string>;
funcs?: Array<(T) => string | string[]>;
shouldMatchWordsOnly?: boolean;
// whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
fuzzy?: boolean;
@ -69,7 +69,12 @@ export default class QueryMatcher<T extends Object> {
if (this._options.funcs) {
for (const f of this._options.funcs) {
keyValues.push(f(object));
const v = f(object);
if (Array.isArray(v)) {
keyValues.push(...v);
} else {
keyValues.push(v);
}
}
}
@ -87,7 +92,7 @@ export default class QueryMatcher<T extends Object> {
}
}
match(query: string): T[] {
match(query: string, limit = -1): T[] {
query = this.processQuery(query);
if (this._options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, '');
@ -129,7 +134,10 @@ export default class QueryMatcher<T extends Object> {
});
// 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 {

View File

@ -1,8 +1,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
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");
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.
*/
import React from 'react';
import React from "react";
import {uniqBy, sortBy} from "lodash";
import Room from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import {MatrixClientPeg} from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components';
import * as sdk from '../index';
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
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;
@ -49,7 +50,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") {
}
export default class RoomProvider extends AutocompleteProvider {
matcher: QueryMatcher<Room>;
protected matcher: QueryMatcher<Room>;
constructor() {
super(ROOM_REGEX);
@ -58,15 +59,28 @@ export default class RoomProvider extends AutocompleteProvider {
});
}
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
protected getRooms() {
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 = [];
const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) {
// 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()) {
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
}
@ -90,7 +104,7 @@ export default class RoomProvider extends AutocompleteProvider {
this.matcher.setObjects(matcherObjects);
const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [
(c) => score(matchedString, c.displayedAlias),
(c) => c.displayedAlias.length,
@ -110,7 +124,7 @@ export default class RoomProvider extends AutocompleteProvider {
),
range,
};
}).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4);
}).filter((completion) => !!completion.completion && completion.completion.length > 0);
}
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;
};
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');
// lazy-load user list into matcher
@ -118,7 +123,7 @@ export default class UserProvider extends AutocompleteProvider {
if (fullMatch && fullMatch !== '@') {
// 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;
completions = this.matcher.match(query).map((user) => {
completions = this.matcher.match(query, limit).map((user) => {
const displayName = (user.name || user.userId || '');
return {
// 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) => {
// don't let keyboard handling escape the context menu
ev.stopPropagation();
if (!this.props.managed) {
if (ev.key === Key.ESCAPE) {
this.props.onFinished();
ev.stopPropagation();
ev.preventDefault();
}
return;
@ -258,7 +260,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
if (handled) {
// consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault();
}
};

View File

@ -50,6 +50,9 @@ class FilePanel extends React.Component {
if (room?.roomId !== this.props?.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
const client = MatrixClientPeg.get();
client.decryptEventIfNeeded(ev);
if (ev.isBeingDecrypted()) {
this.decryptingEvents.add(ev.getId());
} else {
@ -200,10 +203,10 @@ class FilePanel extends React.Component {
previousPhase={RightPanelPhases.RoomSummary}
>
<div className="mx_RoomView_empty">
{ _t("You must <a>register</a> to use this functionality",
{},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
}
{ _t("You must <a>register</a> to use this functionality",
{},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
}
</div>
</BaseCard>;
} else if (this.noRoom) {

View File

@ -123,12 +123,19 @@ class GroupFilterPanel extends React.Component {
mx_GroupFilterPanel_items_selected: itemsSelected,
});
let betaDot;
if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) {
betaDot = <div className="mx_BetaDot" />;
}
let createButton = (
<ActionButton
tooltip
label={_t("Communities")}
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")) {
@ -153,17 +160,17 @@ class GroupFilterPanel extends React.Component {
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div
className="mx_GroupFilterPanel_tagTileContainer"
ref={provided.innerRef}
>
{ this.renderGlobalIcon() }
{ tags }
<div>
{createButton}
</div>
{ provided.placeholder }
<div
className="mx_GroupFilterPanel_tagTileContainer"
ref={provided.innerRef}
>
{ this.renderGlobalIcon() }
{ tags }
<div>
{createButton}
</div>
{ provided.placeholder }
</div>
) }
</Droppable>
</AutoHideScrollbar>

View File

@ -43,7 +43,7 @@ import {mediaFromMxc} from "../../customisations/Media";
import {replaceableComponent} from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
`<h1>HTML for your community's page</h1>
<p>
Use the long description to introduce new members to the community, or distribute
some important <a href="foo">links</a>
@ -110,14 +110,16 @@ class CategoryRoomList extends React.Component {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog(
'Failed to add the following room to the group summary',
'', ErrorDialog,
{
title: _t(
"Failed to add the following rooms to the summary of %(groupId)s:",
{groupId: this.props.groupId},
),
description: errorList.join(", "),
});
'',
ErrorDialog,
{
title: _t(
"Failed to add the following rooms to the summary of %(groupId)s:",
{groupId: this.props.groupId},
),
description: errorList.join(", "),
},
);
});
},
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
@ -146,8 +148,8 @@ class CategoryRoomList extends React.Component {
let catHeader = <div />;
if (this.props.category && this.props.category.profile) {
catHeader = <div className="mx_GroupView_featuredThings_category">
{ this.props.category.profile.name }
</div>;
{ this.props.category.profile.name }
</div>;
}
return <div className="mx_GroupView_featuredThings_container">
{ catHeader }
@ -190,13 +192,14 @@ class FeaturedRoom extends React.Component {
Modal.createTrackedDialog(
'Failed to remove room from group summary',
'', ErrorDialog,
{
title: _t(
"Failed to remove the room from the summary of %(groupId)s",
{groupId: this.props.groupId},
),
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
});
{
title: _t(
"Failed to remove the room from the summary of %(groupId)s",
{groupId: this.props.groupId},
),
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
},
);
});
};
@ -283,13 +286,14 @@ class RoleUserList extends React.Component {
Modal.createTrackedDialog(
'Failed to add the following users to the community summary',
'', ErrorDialog,
{
title: _t(
"Failed to add the following users to the summary of %(groupId)s:",
{groupId: this.props.groupId},
),
description: errorList.join(", "),
});
{
title: _t(
"Failed to add the following users to the summary of %(groupId)s:",
{groupId: this.props.groupId},
),
description: errorList.join(", "),
},
);
});
},
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
@ -299,11 +303,11 @@ class RoleUserList extends React.Component {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
<div className="mx_GroupView_featuredThings_addButton_label">
{ _t('Add a User') }
</div>
</AccessibleButton>) : <div />;
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
<div className="mx_GroupView_featuredThings_addButton_label">
{ _t('Add a User') }
</div>
</AccessibleButton>) : <div />;
const userNodes = this.props.users.map((u) => {
return <FeaturedUser
key={u.user_id}
@ -352,14 +356,16 @@ class FeaturedUser extends React.Component {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog(
'Failed to remove user from community summary',
'', ErrorDialog,
{
title: _t(
"Failed to remove a user from the summary of %(groupId)s",
{groupId: this.props.groupId},
),
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
});
'',
ErrorDialog,
{
title: _t(
"Failed to remove a user from the summary of %(groupId)s",
{groupId: this.props.groupId},
),
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
},
);
});
};
@ -767,8 +773,8 @@ export default class GroupView extends React.Component {
title: _t("Leave Community"),
description: (
<span>
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
{ warnings }
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
{ warnings }
</span>
),
button: _t("Leave"),
@ -1055,10 +1061,11 @@ export default class GroupView extends React.Component {
return null;
}
const membershipButtonClasses = classnames([
'mx_RoomHeader_textButton',
'mx_GroupView_textButton',
],
const membershipButtonClasses = classnames(
[
'mx_RoomHeader_textButton',
'mx_GroupView_textButton',
],
membershipButtonExtraClasses,
);

View File

@ -347,7 +347,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
if (element) {
classes = element.classList;
}
} while (element && !cssClasses.some(c => classes.contains(c)));
} while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
if (element) {
element.focus();
@ -416,7 +416,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const roomList = <RoomList
onKeyDown={this.onKeyDown}
resizeNotifier={null}
resizeNotifier={this.props.resizeNotifier}
onFocus={this.onFocus}
onBlur={this.onBlur}
isMinimized={this.props.isMinimized}

View File

@ -27,7 +27,7 @@ import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
import { IMatrixClientCreds } from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions';
@ -59,6 +59,9 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -119,6 +122,7 @@ interface IState {
usageLimitEventContent?: IUsageLimit;
usageLimitEventTs?: number;
useCompactLayout: boolean;
activeCalls: Array<MatrixCall>;
}
/**
@ -160,6 +164,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
usageLimitDismissed: false,
activeCalls: [],
};
// stash the MatrixClient in case we log out before we are unmounted
@ -175,6 +180,7 @@ class LoggedInView extends React.Component<IProps, IState> {
componentDidMount() {
document.addEventListener('keydown', this._onNativeKeyDown, false);
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._updateServerNoticeEvents();
@ -199,6 +205,7 @@ class LoggedInView extends React.Component<IProps, IState> {
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
@ -206,15 +213,11 @@ class LoggedInView extends React.Component<IProps, IState> {
this.resizer.detach();
}
// 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());
}
private onCallsChanged = () => {
this.setState({
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
});
};
canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) {
@ -661,6 +664,12 @@ class LoggedInView extends React.Component<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
return (
<AudioFeedArrayForCall call={call} key={call.callId} />
);
});
return (
<MatrixClientContext.Provider value={this._matrixClient}>
<div
@ -685,6 +694,7 @@ class LoggedInView extends React.Component<IProps, IState> {
<CallContainer />
<NonUrgentToastContainer />
<HostSignupContainer />
{audioFeedArraysForCalls}
</MatrixClientContext.Provider>
);
}

View File

@ -86,6 +86,8 @@ import {RoomUpdateCause} from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security";
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
/** constants for MatrixChat.state.view */
export enum Views {
// a special initial state which is only used at startup, while we are
@ -484,42 +486,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
startPageChangeTimer() {
// Tor doesn't support performance
if (!performance || !performance.mark) return null;
// This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate
// are used.
if (this.pageChanging) {
console.warn('MatrixChat.startPageChangeTimer: timer already started');
return;
}
this.pageChanging = true;
performance.mark('element_MatrixChat_page_change_start');
PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE);
}
stopPageChangeTimer() {
// Tor doesn't support performance
if (!performance || !performance.mark) return null;
const perfMonitor = PerformanceMonitor.instance;
if (!this.pageChanging) {
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
return;
}
this.pageChanging = false;
performance.mark('element_MatrixChat_page_change_stop');
performance.measure(
'element_MatrixChat_page_change_delta',
'element_MatrixChat_page_change_start',
'element_MatrixChat_page_change_stop',
);
performance.clearMarks('element_MatrixChat_page_change_start');
performance.clearMarks('element_MatrixChat_page_change_stop');
const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop();
perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE);
// In practice, sometimes the entries list is empty, so we get no measurement
if (!measurement) return null;
const entries = perfMonitor.getEntries({
name: PerformanceEntryNames.PAGE_CHANGE,
});
const measurement = entries.pop();
return measurement.duration;
return measurement
? measurement.duration
: null;
}
shouldTrackPageChange(prevState: IState, state: IState) {
@ -740,6 +722,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreenAfterLogin();
break;
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
// what the logic is for each of these branches.
if (this.state.page_type === PageTypes.MyGroups) {
@ -906,6 +890,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let presentedId = roomInfo.room_alias || roomInfo.room_id;
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
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);
if (theAlias) {
presentedId = theAlias;
@ -1094,7 +1083,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private leaveRoomWarnings(roomId: string) {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const isSpace = roomToLeave?.isSpaceRoom();
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications.
const warnings = [];
@ -1133,7 +1122,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId);
const isSpace = roomToLeave?.isSpaceRoom();
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
title: isSpace ? _t("Leave space") : _t("Leave room"),
description: (
@ -1625,11 +1614,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
action: 'start_registration',
params: params,
});
PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER);
} else if (screen === 'login') {
dis.dispatch({
action: 'start_login',
params: params,
});
PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN);
} else if (screen === 'forgot_password') {
dis.dispatch({
action: 'start_password_recovery',
@ -1684,6 +1675,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const type = screen === "start_sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
} else if (screen === 'groups') {
if (SettingsStore.getValue("feature_spaces")) {
dis.dispatch({ action: "view_home_page" });
return;
}
dis.dispatch({
action: 'view_my_groups',
});
@ -1767,6 +1762,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
subAction: params.action,
});
} else if (screen.indexOf('group/') === 0) {
if (SettingsStore.getValue("feature_spaces")) {
dis.dispatch({ action: "view_home_page" });
return;
}
const groupId = screen.substring(6);
// TODO: Check valid group ID
@ -1949,6 +1949,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
await this.postLoginSetup();
PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
};
// complete security / e2e setup has finished

View File

@ -34,6 +34,7 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import {replaceableComponent} from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
@ -427,8 +428,10 @@ export default class MessagePanel extends React.Component {
// we get a new DOM node (restarting the animation) when the ghost
// moves to a different event.
return (
<li key={"_readuptoghost_"+eventId}
className="mx_RoomView_myReadMarker_container">
<li
key={"_readuptoghost_"+eventId}
className="mx_RoomView_myReadMarker_container"
>
{ hr }
</li>
);
@ -469,6 +472,10 @@ export default class MessagePanel extends React.Component {
return {nextEvent, nextTile};
}
get _roomHasPendingEdit() {
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
}
_getEventTiles() {
this.eventNodes = {};
@ -542,11 +549,13 @@ export default class MessagePanel extends React.Component {
}
if (!grouper) {
const wantTile = this._shouldShowEvent(mxEv);
const isGrouped = false;
if (wantTile) {
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped,
nextEvent, nextTile));
prevEvent = mxEv;
}
@ -555,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) {
ret.push(...grouper.getTiles());
}
@ -562,7 +578,7 @@ export default class MessagePanel extends React.Component {
return ret;
}
_getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
_getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
@ -570,7 +586,6 @@ export default class MessagePanel extends React.Component {
const isEditing = this.props.editState &&
this.props.editState.getEvent().getId() === mxEv.getId();
// local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators.
let ts1 = mxEv.getTs();
@ -582,7 +597,7 @@ export default class MessagePanel extends React.Component {
// do we need a date separator since the last event?
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
if (wantsDateSeparator) {
if (wantsDateSeparator && !isGrouped) {
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
ret.push(dateSeparator);
}
@ -966,9 +981,9 @@ class CreationGrouper {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const isGrouped = true;
const createEvent = this.createEvent;
const lastShownEvent = this.lastShownEvent;
@ -982,12 +997,12 @@ class CreationGrouper {
// If this m.room.create event should be shown (room upgrade) then show it before the summary
if (panel._shouldShowEvent(createEvent)) {
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
ret.push(...panel._getTilesForEvent(createEvent, createEvent));
}
for (const ejected of this.ejectedEvents) {
ret.push(...panel._getTilesForEvent(
createEvent, ejected, createEvent === lastShownEvent,
createEvent, ejected, createEvent === lastShownEvent, isGrouped,
));
}
@ -996,7 +1011,7 @@ class CreationGrouper {
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return panel._getTilesForEvent(e, e, e === lastShownEvent);
return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
@ -1014,13 +1029,13 @@ class CreationGrouper {
ret.push(
<EventListSummary
key="roomcreationsummary"
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={summaryText}
key="roomcreationsummary"
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={summaryText}
>
{ eventTiles }
{ eventTiles }
</EventListSummary>,
);
@ -1081,7 +1096,7 @@ class RedactionGrouper {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const isGrouped = true;
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
@ -1101,7 +1116,8 @@ class RedactionGrouper {
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile);
return panel._getTilesForEvent(
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
@ -1180,7 +1196,7 @@ class MemberGrouper {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
const isGrouped = true;
const panel = this.panel;
const lastShownEvent = this.lastShownEvent;
const ret = [];
@ -1213,7 +1229,7 @@ class MemberGrouper {
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return panel._getTilesForEvent(e, e, e === lastShownEvent);
return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
@ -1222,11 +1238,11 @@ class MemberGrouper {
ret.push(
<MemberEventListSummary key={key}
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
>
{ eventTiles }
{ eventTiles }
</MemberEventListSummary>,
);

View File

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

View File

@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore";
@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component {
@ -85,7 +86,9 @@ export default class RightPanel extends React.Component {
return RightPanelPhases.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
} else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom()
&& !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)
) {
return RightPanelPhases.SpaceMemberList;
} else if (userForPanel) {
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state

View File

@ -17,6 +17,8 @@ limitations under the License.
import * as React from "react";
import { createRef } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import { ActionPayload } from "../../dispatcher/payloads";
@ -25,8 +27,8 @@ import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import {replaceableComponent} from "../../utils/replaceableComponent";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
interface IProps {
isMinimized: boolean;
@ -40,6 +42,7 @@ interface IProps {
interface IState {
query: string;
focused: boolean;
inSpaces: boolean;
}
@replaceableComponent("structures.RoomSearch")
@ -54,11 +57,13 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
this.state = {
query: "",
focused: false,
inSpaces: false,
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@ -79,8 +84,15 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
}
private onSpaces = (spaces: Room[]) => {
this.setState({
inSpaces: spaces.length > 0,
});
};
private onAction = (payload: ActionPayload) => {
if (payload.action === 'view_room' && payload.clear_search) {
this.clearInput();
@ -152,6 +164,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
});
let placeholder = _t("Filter");
if (this.state.inSpaces) {
placeholder = _t("Filter all spaces");
}
let icon = (
<div className='mx_RoomSearch_icon' />
);
@ -165,7 +182,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onBlur={this.onBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
placeholder={_t("Filter")}
placeholder={placeholder}
autoComplete="off"
/>
);

View File

@ -200,20 +200,22 @@ export default class RoomStatusBar extends React.Component {
} else if (resourceLimitError) {
title = messageForResourceLimitError(
resourceLimitError.data.limit_type,
resourceLimitError.data.admin_contact, {
'monthly_active_user': _td(
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'hs_disabled': _td(
"Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'': _td(
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
});
resourceLimitError.data.admin_contact,
{
'monthly_active_user': _td(
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'hs_disabled': _td(
"Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'': _td(
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
},
);
} else {
title = _t('Some of your messages have not been sent');
}
@ -265,7 +267,7 @@ export default class RoomStatusBar extends React.Component {
<div role="alert">
<div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24"
height="24" title="/!\ " alt="/!\ " />
height="24" title="/!\ " alt="/!\ " />
<div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
{_t('Connectivity to the server has been lost.')}

View File

@ -190,6 +190,9 @@ export interface IState {
rejectError?: Error;
hasPinnedWidgets?: boolean;
dragCounter: number;
// whether or not a spaces context switch brought us here,
// if it did we don't want the room to be marked as read as soon as it is loaded.
wasContextSwitch?: boolean;
}
@replaceableComponent("structures.RoomView")
@ -326,6 +329,7 @@ export default class RoomView extends React.Component<IProps, IState> {
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
};
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
@ -807,7 +811,7 @@ export default class RoomView extends React.Component<IProps, IState> {
};
private onEvent = (ev) => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
this.handleEffects(ev);
};
@ -1594,33 +1598,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
};
private onFullscreenClick = () => {
dis.dispatch({
action: 'video_fullscreen',
fullscreen: true,
}, true);
};
private onMuteAudioClick = () => {
const call = this.getCallForRoom();
if (!call) {
return;
}
const newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons
};
private onMuteVideoClick = () => {
const call = this.getCallForRoom();
if (!call) {
return;
}
const newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons
};
private onStatusBarVisible = () => {
if (this.unmounted) return;
this.setState({
@ -1636,24 +1613,6 @@ export default class RoomView extends React.Component<IProps, IState> {
});
};
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
* We pass it down to the scroll panel.
*/
private handleScrollKey = ev => {
let panel;
if (this.searchResultsPanel.current) {
panel = this.searchResultsPanel.current;
} else if (this.messagePanel) {
panel = this.messagePanel;
}
if (panel) {
panel.handleScrollKey(ev);
}
};
/**
* get any current call for this room
*/
@ -1746,7 +1705,10 @@ export default class RoomView extends React.Component<IProps, IState> {
}
const myMembership = this.state.room.getMyMembership();
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
if (myMembership === "invite"
// SpaceRoomView handles invites itself
&& (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom())
) {
if (this.state.joining || this.state.rejecting) {
return (
<ErrorBoundary>
@ -1888,7 +1850,7 @@ export default class RoomView extends React.Component<IProps, IState> {
room={this.state.room}
/>
);
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) {
return (
<div className="mx_RoomView">
{ previewBar }
@ -1910,7 +1872,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
space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts}
@ -2014,6 +1976,7 @@ export default class RoomView extends React.Component<IProps, IState> {
timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}

View File

@ -884,16 +884,20 @@ export default class ScrollPanel extends React.Component {
// give the <ol> an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
// list-style-type: none; is no longer a list
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
return (
<AutoHideScrollbar
wrappedRef={this._collectScroll}
onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children }
</ol>
</div>
</AutoHideScrollbar>
);
className={`mx_ScrollPanel ${this.props.className}`}
style={this.props.style}
>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children }
</ol>
</div>
</AutoHideScrollbar>
);
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useMemo, useState} from "react";
import React, {ReactNode, useMemo, useState} from "react";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
@ -24,7 +24,7 @@ import {sortBy} from "lodash";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import {_t} from "../../languageHandler";
import AccessibleButton from "../views/elements/AccessibleButton";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog";
import Spinner from "../views/elements/Spinner";
import SearchBox from "./SearchBox";
@ -39,11 +39,15 @@ import {mediaFromMxc} from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle";
import {getOrder} from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import {linkifyElement} from "../../HtmlUtils";
interface IHierarchyProps {
space: Room;
initialText?: string;
refreshToken?: any;
additionalButtons?: ReactNode;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
}
@ -72,7 +76,7 @@ export interface ISpaceSummaryEvent {
order?: string;
suggested?: boolean;
auto_join?: boolean;
via?: string;
via?: string[];
};
}
/* eslint-enable camelcase */
@ -106,8 +110,16 @@ const Tile: React.FC<ITileProps> = ({
const cliRoom = cli.getRoom(room.room_id);
const myMembership = cliRoom?.getMyMembership();
const onPreviewClick = () => onViewRoomClick(false);
const onJoinClick = () => onViewRoomClick(true);
const onPreviewClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
onViewRoomClick(false);
}
const onJoinClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
onViewRoomClick(true);
}
let button;
if (myMembership === "join") {
@ -140,7 +152,7 @@ const Tile: React.FC<ITileProps> = ({
}
let description = _t("%(count)s members", { count: room.num_joined_members });
if (numChildRooms) {
if (numChildRooms !== undefined) {
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
}
if (room.topic) {
@ -161,7 +173,16 @@ const Tile: React.FC<ITileProps> = ({
{ suggestedSection }
</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 }
</div>
<div className="mx_SpaceRoomDirectory_actions">
@ -254,7 +275,11 @@ export const HierarchyLevel = ({
const space = cli.getRoom(spaceId);
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
const children = Array.from(relations.get(spaceId)?.values() || []);
const sortedChildren = sortBy(children, ev => {
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
return getOrder(ev.content.order, null, ev.state_key);
});
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key;
if (!rooms.has(roomId)) return result;
@ -331,9 +356,9 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id);
}
if (Array.isArray(ev.content["via"])) {
if (Array.isArray(ev.content.via)) {
const set = viaMap.getOrCreate(ev.state_key, new Set());
ev.content["via"].forEach(via => set.add(via));
ev.content.via.forEach(via => set.add(via));
}
});
@ -350,6 +375,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
initialText = "",
showRoom,
refreshToken,
additionalButtons,
children,
}) => {
const cli = MatrixClientPeg.get();
@ -415,78 +441,87 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
}
let editSection;
let manageButtons;
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
});
let buttons;
if (selectedRelations.length) {
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
});
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
});
const disabled = removing || saving;
const disabled = !selectedRelations.length || removing || saving;
buttons = <>
<AccessibleButton
onClick={async () => {
setRemoving(true);
try {
for (const [parentId, childId] of selectedRelations) {
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
parentChildMap.get(parentId).get(childId).content = {};
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
}
} catch (e) {
setError(_t("Failed to remove some rooms. Try again later"));
}
setRemoving(false);
}}
kind="danger_outline"
disabled={disabled}
>
{ removing ? _t("Removing...") : _t("Remove") }
</AccessibleButton>
<AccessibleButton
onClick={async () => {
setSaving(true);
try {
for (const [parentId, childId] of selectedRelations) {
const suggested = !selectionAllSuggested;
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
if (!existingContent || existingContent.suggested === suggested) continue;
const content = {
...existingContent,
suggested: !selectionAllSuggested,
};
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
parentChildMap.get(parentId).get(childId).content = content;
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
}
} catch (e) {
setError("Failed to update some suggestions. Try again later");
}
setSaving(false);
}}
kind="primary_outline"
disabled={disabled}
>
{ saving
? _t("Saving...")
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
}
</AccessibleButton>
</>;
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
let props = {};
if (!selectedRelations.length) {
Button = AccessibleTooltipButton;
props = {
tooltip: _t("Select a room below first"),
yOffset: -40,
};
}
editSection = <span>
{ buttons }
</span>;
manageButtons = <>
<Button
{...props}
onClick={async () => {
setRemoving(true);
try {
for (const [parentId, childId] of selectedRelations) {
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
parentChildMap.get(parentId).delete(childId);
if (parentChildMap.get(parentId).size > 0) {
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
} else {
parentChildMap.delete(parentId);
}
}
} catch (e) {
setError(_t("Failed to remove some rooms. Try again later"));
}
setRemoving(false);
}}
kind="danger_outline"
disabled={disabled}
>
{ removing ? _t("Removing...") : _t("Remove") }
</Button>
<Button
{...props}
onClick={async () => {
setSaving(true);
try {
for (const [parentId, childId] of selectedRelations) {
const suggested = !selectionAllSuggested;
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
if (!existingContent || existingContent.suggested === suggested) continue;
const content = {
...existingContent,
suggested: !selectionAllSuggested,
};
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
parentChildMap.get(parentId).get(childId).content = content;
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
}
} catch (e) {
setError("Failed to update some suggestions. Try again later");
}
setSaving(false);
}}
kind="primary_outline"
disabled={disabled}
>
{ saving
? _t("Saving...")
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
}
</Button>
</>;
}
let results;
@ -532,7 +567,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
content = <>
<div className="mx_SpaceRoomDirectory_listHeader">
{ countsStr }
{ editSection }
<span>
{ additionalButtons }
{ manageButtons }
</span>
</div>
{ error && <div className="mx_SpaceRoomDirectory_error">
{ error }
@ -550,7 +588,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
return <>
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Search names and description") }
placeholder={ _t("Search names and descriptions") }
onSearch={setQuery}
autoFocus={true}
initialValue={initialText}

View File

@ -52,8 +52,19 @@ import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile";
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
import {allSettled} from "../../utils/promise";
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import {BetaPill} from "../views/beta/BetaCard";
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
import 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 {
space: Room;
@ -65,6 +76,7 @@ interface IProps {
interface IState {
phase: Phase;
createdRooms?: boolean; // internal state for the creation wizard
showRightPanel: boolean;
myMembership: string;
}
@ -79,6 +91,26 @@ enum Phase {
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 members = useRoomMembers(room);
const count = members.length;
@ -130,15 +162,39 @@ const SpaceInfo = ({ space }) => {
</div>
};
const onBetaClick = () => {
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB,
});
};
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
const [busy, setBusy] = useState(false);
const spacesEnabled = SettingsStore.getValue("feature_spaces");
let inviterSection;
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 inviter = inviteSender && space.getMember(inviteSender);
@ -174,6 +230,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
setBusy(true);
onJoinButtonClicked();
}}
disabled={!spacesEnabled}
>
{ _t("Accept") }
</AccessibleButton>
@ -186,10 +243,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
setBusy(true);
onJoinButtonClicked();
}}
disabled={!spacesEnabled}
>
{ _t("Join") }
</AccessibleButton>
)
);
}
if (busy) {
@ -197,6 +255,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
}
return <div className="mx_SpaceRoomView_preview">
<BetaPill onClick={onBetaClick} />
{ inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name">
@ -214,9 +273,84 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
<div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons }
</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>;
};
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
const cli = useContext(MatrixClientContext);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu;
if (menuDisplayed) {
const rect = handle.current.getBoundingClientRect();
contextMenu = <IconizedContextMenu
left={rect.left + window.pageXOffset + 0}
top={rect.bottom + window.pageYOffset + 8}
chevronFace={ChevronFace.None}
onFinished={closeMenu}
className="mx_RoomTile_contextMenu"
compact
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
if (await showCreateNewRoom(cli, space)) {
onNewRoomAdded();
}
}}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
const [added] = await showAddExistingRooms(cli, space);
if (added) {
onNewRoomAdded();
}
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
return <>
<ContextMenuButton
kind="primary"
inputRef={handle}
onClick={openMenu}
isExpanded={menuDisplayed}
label={_t("Add")}
>
{ _t("Add") }
</ContextMenuButton>
{ contextMenu }
</>;
};
const SpaceLanding = ({ space }) => {
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
@ -241,32 +375,20 @@ const SpaceLanding = ({ space }) => {
const [refreshToken, forceUpdate] = useStateToggle(false);
let addRoomButtons;
let addRoomButton;
if (canAddRooms) {
addRoomButtons = <React.Fragment>
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
const [added] = await showAddExistingRooms(cli, space);
if (added) {
forceUpdate();
}
}}>
{ _t("Add existing rooms & spaces") }
</AccessibleButton>
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
showCreateNewRoom(cli, space);
}}>
{ _t("Create a new room") }
</AccessibleButton>
</React.Fragment>;
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
}
let settingsButton;
if (shouldShowSpaceSettings(cli, space)) {
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
showSpaceSettings(cli, space);
}}>
{ _t("Settings") }
</AccessibleButton>;
settingsButton = <AccessibleTooltipButton
className="mx_SpaceRoomView_landing_settingsButton"
onClick={() => {
showSpaceSettings(cli, space);
}}
title={_t("Settings")}
/>;
}
const onMembersClick = () => {
@ -293,17 +415,20 @@ const SpaceLanding = ({ space }) => {
<SpaceInfo space={space} />
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
{ inviteButton }
{ settingsButton }
</div>
<div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} />
</div>
<SpaceFeedbackPrompt />
<hr />
<div className="mx_SpaceRoomView_landing_adminButtons">
{ addRoomButtons }
{ settingsButton }
</div>
<SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
<SpaceHierarchy
space={space}
showRoom={showRoom}
refreshToken={refreshToken}
additionalButtons={addRoomButton}
/>
</div>;
};
@ -325,14 +450,18 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
value={roomNames[i]}
onChange={ev => setRoomName(i, ev.target.value)}
autoFocus={i === 2}
disabled={busy}
/>;
});
const onNextClick = async () => {
const onNextClick = async (ev) => {
ev.preventDefault();
if (busy) return;
setError("");
setBusy(true);
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({
createOpts: {
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
@ -345,7 +474,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
parentSpace: space,
});
}));
onFinished();
onFinished(filteredRoomNames.length > 0);
} catch (e) {
console.error("Failed to create initial space rooms", e);
setError(_t("Failed to create initial space rooms"));
@ -353,7 +482,10 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
setBusy(false);
};
let onClick = onFinished;
let onClick = (ev) => {
ev.preventDefault();
onFinished(false);
};
let buttonLabel = _t("Skip for now");
if (roomNames.some(name => name.trim())) {
onClick = onNextClick;
@ -365,45 +497,26 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
<div className="mx_SpaceRoomView_description">{ description }</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
{ fields }
<form onSubmit={onClick} id="mx_SpaceSetupFirstRooms">
{ fields }
</form>
<div className="mx_SpaceRoomView_buttons">
<AccessibleButton
kind="primary"
disabled={busy}
onClick={onClick}
>
{ buttonLabel }
</AccessibleButton>
element="input"
type="submit"
form="mx_SpaceSetupFirstRooms"
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};
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 () => {
// TODO rate limiting
setBusy(true);
try {
await allSettled(Array.from(selectedToAdd).map((room) =>
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
onFinished(true);
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space"));
}
setBusy(false);
};
buttonLabel = busy ? _t("Adding...") : _t("Add");
}
return <div>
<h1>{ _t("What do you want to organise?") }</h1>
<div className="mx_SpaceRoomView_description">
@ -411,36 +524,28 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
"no one will be informed. You can add more later.") }
</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<AddExistingToSpace
space={space}
selected={selectedToAdd}
onChange={(checked, room) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
emptySelectionButton={
<AccessibleButton kind="primary" onClick={onFinished}>
{ _t("Skip for now") }
</AccessibleButton>
}
onFinished={onFinished}
/>
<div className="mx_SpaceRoomView_buttons">
<AccessibleButton
kind="primary"
disabled={busy}
onClick={onClick}
>
{ buttonLabel }
</AccessibleButton>
</div>
<SpaceFeedbackPrompt />
</div>;
};
const SpaceSetupPublicShare = ({ space, onFinished }) => {
const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => {
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">
{ _t("It's just you at the moment, it will be even better with others.") }
</div>
@ -449,17 +554,20 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => {
<div className="mx_SpaceRoomView_buttons">
<AccessibleButton kind="primary" onClick={onFinished}>
{ _t("Go to my first room") }
{ createdRooms ? _t("Go to my first room") : _t("Go to my space") }
</AccessibleButton>
</div>
<SpaceFeedbackPrompt />
</div>;
};
const SpaceSetupPrivateScope = ({ space, onFinished }) => {
const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
return <div className="mx_SpaceRoomView_privateScope">
<h1>{ _t("Who are you working with?") }</h1>
<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>
<AccessibleButton
@ -476,6 +584,7 @@ const SpaceSetupPrivateScope = ({ space, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton>
<SpaceFeedbackPrompt />
</div>;
};
@ -506,10 +615,13 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
ref={fieldRefs[i]}
onValidate={validateEmailRules}
autoFocus={i === 0}
disabled={busy}
/>;
});
const onNextClick = async () => {
const onNextClick = async (ev) => {
ev.preventDefault();
if (busy) return;
setError("");
for (let i = 0; i < fieldRefs.length; i++) {
const fieldRef = fieldRefs[i];
@ -543,7 +655,10 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
setBusy(false);
};
let onClick = onFinished;
let onClick = (ev) => {
ev.preventDefault();
onFinished();
};
let buttonLabel = _t("Skip for now");
if (emailAddresses.some(name => name.trim())) {
onClick = onNextClick;
@ -556,8 +671,21 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
{ _t("Make sure the right people have access. You can invite more later.") }
</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> }
{ fields }
<form onSubmit={onClick} id="mx_SpaceSetupPrivateInvite">
{ fields }
</form>
<div className="mx_SpaceRoomView_inviteTeammates_buttons">
<AccessibleButton
@ -569,10 +697,17 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
</div>
<div className="mx_SpaceRoomView_buttons">
<AccessibleButton kind="primary" disabled={busy} onClick={onClick}>
{ buttonLabel }
</AccessibleButton>
<AccessibleButton
kind="primary"
disabled={busy}
onClick={onClick}
element="input"
type="submit"
form="mx_SpaceSetupPrivateInvite"
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -671,7 +806,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
let suggestedRooms = SpaceStore.instance.suggestedRooms;
if (SpaceStore.instance.activeSpace !== this.props.space) {
// the space store has the suggested rooms loaded for a different space, fetch the right ones
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms;
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1));
}
if (suggestedRooms.length) {
@ -679,9 +814,11 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
defaultDispatcher.dispatch({
action: "view_room",
room_id: room.room_id,
room_alias: room.canonical_alias || room.aliases?.[0],
via_servers: room.viaServers,
oobData: {
avatarUrl: room.avatar_url,
name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"),
name: room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"),
},
});
return;
@ -693,7 +830,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() {
switch (this.state.phase) {
case Phase.Landing:
if (this.state.myMembership === "join") {
if (this.state.myMembership === "join" && SettingsStore.getValue("feature_spaces")) {
return <SpaceLanding space={this.props.space} />;
} else {
return <SpacePreview
@ -706,20 +843,26 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
return <SpaceSetupFirstRooms
space={this.props.space}
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={
_t("Let's create a room for each of them.") + "\n" +
_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:
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:
return <SpaceSetupPrivateScope
space={this.props.space}
justCreatedOpts={this.props.justCreatedOpts}
onFinished={(invite: boolean) => {
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms });
}}
@ -735,7 +878,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
title={_t("What projects are you working on?")}
description={_t("We'll create rooms for each of them. " +
"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:
return <SpaceAddExistingRooms

View File

@ -38,6 +38,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature";
import {objectHasDiff} from "../../utils/objects";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -68,6 +69,7 @@ class TimelinePanel extends React.Component {
showReadReceipts: PropTypes.bool,
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: PropTypes.bool,
sendReadReceiptOnLoad: PropTypes.bool,
manageReadMarkers: PropTypes.bool,
// true to give the component a 'display: none' style.
@ -126,6 +128,7 @@ class TimelinePanel extends React.Component {
// event tile heights. (See _unpaginateEvents)
timelineCap: Number.MAX_VALUE,
className: 'mx_RoomView_messagePanel',
sendReadReceiptOnLoad: true,
};
constructor(props) {
@ -785,8 +788,10 @@ class TimelinePanel extends React.Component {
return;
}
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
this._setReadMarker(lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs());
this._setReadMarker(
lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs(),
);
// the read-marker should become invisible, so that if the user scrolls
// down, they don't see it.
@ -872,7 +877,7 @@ class TimelinePanel extends React.Component {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
0, 1/3);
0, 1/3);
return;
}
@ -1044,12 +1049,14 @@ class TimelinePanel extends React.Component {
}
if (eventId) {
this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase);
offsetBase);
} else {
this._messagePanel.current.scrollToBottom();
}
this.sendReadReceipt();
if (this.props.sendReadReceiptOnLoad) {
this.sendReadReceipt();
}
});
};
@ -1135,6 +1142,17 @@ class TimelinePanel extends React.Component {
// get the list of events from the timeline window and the pending event list
_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 => {
const client = MatrixClientPeg.get();
client.decryptEventIfNeeded(event);
});
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
// Hold onto the live events separately. The read receipt and read marker
@ -1418,8 +1436,8 @@ class TimelinePanel extends React.Component {
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
);
const events = this.state.firstVisibleEventIndex
? this.state.events.slice(this.state.firstVisibleEventIndex)
: this.state.events;
? this.state.events.slice(this.state.firstVisibleEventIndex)
: this.state.events;
return (
<MessagePanel
ref={this._messagePanel}

View File

@ -169,7 +169,7 @@ export class PasswordAuthEntry extends React.Component {
{ submitButtonOrSpinner }
</div>
</form>
{ errorSection }
{ errorSection }
</div>
);
}
@ -375,7 +375,7 @@ export class TermsAuthEntry extends React.Component {
if (this.props.showContinue !== false) {
// XXX: button classes
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
}
return (

View File

@ -179,7 +179,7 @@ const BaseAvatar = (props: IProps) => {
width: toPx(width),
height: toPx(height),
}}
title={title} alt=""
title={title} alt={_t("Avatar")}
inputRef={inputRef}
{...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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar";
import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
room: Room;
avatarSize: number;
tag: TagID;
displayBadge?: boolean;
forceCount?: boolean;
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

@ -78,8 +78,10 @@ export default class MessageContextMenu extends React.Component {
// We explicitly decline to show the redact option on ACL events as it has a potential
// to obliterate the room - https://github.com/matrix-org/synapse/issues/4042
// Similarly for encryption events, since redacting them "breaks everything"
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl;
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
@ -350,7 +352,7 @@ export default class MessageContextMenu extends React.Component {
>
{ _t('Source URL') }
</MenuItem>
);
);
}
if (this.props.collapseReplyThread) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext, useState} from "react";
import React, {ReactNode, useContext, useMemo, useState} from "react";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
@ -29,11 +29,16 @@ import RoomAvatar from "../avatars/RoomAvatar";
import {getDisplayAliasForRoom} from "../../../Rooms";
import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {allSettled} from "../../../utils/promise";
import {sleep} from "../../../utils/promise";
import DMRoomMap from "../../../utils/DMRoomMap";
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@ -43,46 +48,161 @@ interface IProps extends IDialogProps {
const Entry = ({ room, checked, onChange }) => {
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>
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
<StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null}
checked={checked}
disabled={!onChange}
/>
</label>;
};
interface IAddExistingToSpaceProps {
space: Room;
selected: Set<Room>;
onChange(checked: boolean, room: Room): void;
footerPrompt?: ReactNode;
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 visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [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 lcQuery = query.toLowerCase();
const lcQuery = query.toLowerCase().trim();
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const existingSubspacesSet = new Set(existingSubspaces);
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
const existingSubspacesSet = useMemo(() => new Set(SpaceStore.instance.getChildSpaces(space.roomId)), [space]);
const existingRoomsSet = useMemo(() => new Set(SpaceStore.instance.getChildRooms(space.roomId)), [space]);
const joinRule = space.getJoinRule();
const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => {
if (room.getMyMembership() !== "join") return arr;
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
const [spaces, rooms, dms] = useMemo(() => {
let rooms = visibleRooms;
if (room.isSpaceRoom()) {
if (room !== space && !existingSubspacesSet.has(room)) {
arr[0].push(room);
if (lcQuery) {
const matcher = new QueryMatcher<Room>(visibleRooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
});
rooms = matcher.match(lcQuery);
}
const joinRule = space.getJoinRule();
return sortRooms(rooms).reduce((arr, room) => {
if (room.isSpaceRoom()) {
if (room !== space && !existingSubspacesSet.has(room)) {
arr[0].push(room);
}
} else if (!existingRoomsSet.has(room)) {
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
arr[1].push(room);
} else if (joinRule !== "public") {
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
arr[2].push(room);
}
}
} else if (!existingRoomsSet.has(room)) {
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
arr[1].push(room);
} else if (joinRule !== "public") {
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
arr[2].push(room);
return arr;
}, [[], [], []]);
}, [visibleRooms, space, lcQuery, existingRoomsSet, existingSubspacesSet]);
const addRooms = async () => {
setError(null);
setProgress(0);
let error;
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;
});
setProgress(i => i + 1);
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(error = e);
break;
}
}
return arr;
}, [[], [], []]);
if (!error) {
onFinished(true);
}
};
const busy = progress !== null;
let footer;
if (error) {
footer = <>
<img
src={require("../../../../res/img/element-icons/warning-badge.svg")}
height="24"
width="24"
alt=""
/>
<span className="mx_AddExistingToSpaceDialog_error">
<div className="mx_AddExistingToSpaceDialog_errorHeading">{ _t("Not all selected were added") }</div>
<div className="mx_AddExistingToSpaceDialog_errorCaption">{ _t("Try again") }</div>
</span>
<AccessibleButton className="mx_AddExistingToSpaceDialog_retryButton" onClick={addRooms}>
{ _t("Retry") }
</AccessibleButton>
</>;
} else if (busy) {
footer = <span>
<ProgressBar value={progress} max={selectedToAdd.size} />
<div className="mx_AddExistingToSpaceDialog_progressText">
{ _t("Adding rooms... (%(progress)s out of %(count)s)", {
count: selectedToAdd.size,
progress,
}) }
</div>
</span>;
} else {
let button = emptySelectionButton;
if (!button || selectedToAdd.size > 0) {
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
{ _t("Add") }
</AccessibleButton>;
}
footer = <>
<span>
{ footerPrompt }
</span>
{ button }
</>;
}
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
@ -100,10 +220,10 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={(checked) => {
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
}}
} : null}
/>;
}) }
</div>
@ -112,14 +232,18 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
{ 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={selected.has(space)}
onChange={(checked) => {
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
}}
} : null}
/>;
}) }
</div>
@ -132,10 +256,10 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={(checked) => {
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
}}
} : null}
/>;
}) }
</div>
@ -145,16 +269,16 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
{ _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);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
let spaceOptionSection;
if (existingSubspaces.length > 0) {
@ -201,51 +325,20 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
onFinished={onFinished}
fixedWidth={false}
>
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
<MatrixClientContext.Provider value={cli}>
<AddExistingToSpace
space={space}
selected={selectedToAdd}
onChange={(checked, room) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
onFinished={onFinished}
footerPrompt={<>
<div>{ _t("Want to add a new room instead?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</>}
/>
</MatrixClientContext.Provider>
<div className="mx_AddExistingToSpaceDialog_footer">
<span>
<div>{ _t("Don't want to add an existing room?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</span>
<AccessibleButton
kind="primary"
disabled={busy || selectedToAdd.size < 1}
onClick={async () => {
// TODO rate limiting
setBusy(true);
try {
await allSettled(Array.from(selectedToAdd).map((room) =>
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
onFinished(true);
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space"));
}
setBusy(false);
}}
>
{ busy ? _t("Adding...") : _t("Add") }
</AccessibleButton>
</div>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</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

@ -184,7 +184,7 @@ export default class BugReportDialog extends React.Component {
return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>

View File

@ -95,7 +95,7 @@ export default class ChangelogDialog extends React.Component {
description={content}
button={_t("Update")}
onFinished={this.props.onFinished}
/>
/>
);
}
}

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