Merge branch 'develop' into travis/message-states
commit
c74224a1b7
78
CHANGELOG.md
78
CHANGELOG.md
|
@ -1,3 +1,81 @@
|
|||
Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0)
|
||||
|
||||
## Security notice
|
||||
|
||||
matrix-react-sdk 3.15.0 fixes a low severity issue (CVE-2021-21320) where the
|
||||
user content sandbox can be abused to trick users into opening unexpected
|
||||
documents. The content is opened with a `blob` origin that cannot access Matrix
|
||||
user data, so messages and secrets are not at risk. Thanks to @keerok for
|
||||
responsibly disclosing this via Matrix's Security Disclosure Policy.
|
||||
|
||||
## All changes
|
||||
|
||||
* Upgrade to JS SDK 9.8.0
|
||||
|
||||
Changes in [3.15.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0-rc.1) (2021-02-24)
|
||||
===============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0...v3.15.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 9.8.0-rc.1
|
||||
* Translations update from Weblate
|
||||
[\#5683](https://github.com/matrix-org/matrix-react-sdk/pull/5683)
|
||||
* Fix object diffing when objects have different keys
|
||||
[\#5681](https://github.com/matrix-org/matrix-react-sdk/pull/5681)
|
||||
* Add <code> if it's missing
|
||||
[\#5673](https://github.com/matrix-org/matrix-react-sdk/pull/5673)
|
||||
* Add email only if the verification is complete
|
||||
[\#5629](https://github.com/matrix-org/matrix-react-sdk/pull/5629)
|
||||
* Fix portrait videocalls
|
||||
[\#5676](https://github.com/matrix-org/matrix-react-sdk/pull/5676)
|
||||
* Tweak code block icon positions
|
||||
[\#5643](https://github.com/matrix-org/matrix-react-sdk/pull/5643)
|
||||
* Revert "Improve URL preview formatting and image upload thumbnail size"
|
||||
[\#5677](https://github.com/matrix-org/matrix-react-sdk/pull/5677)
|
||||
* Fix context menu leaving visible area
|
||||
[\#5644](https://github.com/matrix-org/matrix-react-sdk/pull/5644)
|
||||
* Jitsi conferences names, take 3
|
||||
[\#5675](https://github.com/matrix-org/matrix-react-sdk/pull/5675)
|
||||
* Update isUserOnDarkTheme to take use_system_theme in account
|
||||
[\#5670](https://github.com/matrix-org/matrix-react-sdk/pull/5670)
|
||||
* Discard some dead code
|
||||
[\#5665](https://github.com/matrix-org/matrix-react-sdk/pull/5665)
|
||||
* Add developer tool to explore and edit settings
|
||||
[\#5664](https://github.com/matrix-org/matrix-react-sdk/pull/5664)
|
||||
* Use and create new room helpers
|
||||
[\#5663](https://github.com/matrix-org/matrix-react-sdk/pull/5663)
|
||||
* Clear message previews when the maximum limit is reached for history
|
||||
[\#5661](https://github.com/matrix-org/matrix-react-sdk/pull/5661)
|
||||
* VoIP virtual rooms, mk II
|
||||
[\#5639](https://github.com/matrix-org/matrix-react-sdk/pull/5639)
|
||||
* Disable chat effects when reduced motion preferred
|
||||
[\#5660](https://github.com/matrix-org/matrix-react-sdk/pull/5660)
|
||||
* Improve URL preview formatting and image upload thumbnail size
|
||||
[\#5637](https://github.com/matrix-org/matrix-react-sdk/pull/5637)
|
||||
* Fix border radius when the panel is collapsed
|
||||
[\#5641](https://github.com/matrix-org/matrix-react-sdk/pull/5641)
|
||||
* Use a more generic layout setting - useIRCLayout → layout
|
||||
[\#5571](https://github.com/matrix-org/matrix-react-sdk/pull/5571)
|
||||
* Remove redundant lockOrigin parameter from usercontent
|
||||
[\#5657](https://github.com/matrix-org/matrix-react-sdk/pull/5657)
|
||||
* Set ICE candidate pool size option
|
||||
[\#5655](https://github.com/matrix-org/matrix-react-sdk/pull/5655)
|
||||
* Prepare to encrypt when a call arrives
|
||||
[\#5654](https://github.com/matrix-org/matrix-react-sdk/pull/5654)
|
||||
* Use config for host signup branding
|
||||
[\#5650](https://github.com/matrix-org/matrix-react-sdk/pull/5650)
|
||||
* Use randomly generated conference names for Jitsi
|
||||
[\#5649](https://github.com/matrix-org/matrix-react-sdk/pull/5649)
|
||||
* Modified regex to account for an immediate new line after slash commands
|
||||
[\#5647](https://github.com/matrix-org/matrix-react-sdk/pull/5647)
|
||||
* Fix codeblock scrollbar color for non-Firefox
|
||||
[\#5642](https://github.com/matrix-org/matrix-react-sdk/pull/5642)
|
||||
* Fix codeblock scrollbar colors
|
||||
[\#5630](https://github.com/matrix-org/matrix-react-sdk/pull/5630)
|
||||
* Added loading and disabled the button while searching for server
|
||||
[\#5634](https://github.com/matrix-org/matrix-react-sdk/pull/5634)
|
||||
|
||||
Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.14.0",
|
||||
"version": "3.15.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
@import "./structures/_RoomView.scss";
|
||||
@import "./structures/_ScrollPanel.scss";
|
||||
@import "./structures/_SearchBox.scss";
|
||||
@import "./structures/_SpacePanel.scss";
|
||||
@import "./structures/_SpaceRoomView.scss";
|
||||
@import "./structures/_TabbedView.scss";
|
||||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
|
@ -212,6 +214,7 @@
|
|||
@import "./views/settings/_DevicesPanel.scss";
|
||||
@import "./views/settings/_E2eAdvancedPanel.scss";
|
||||
@import "./views/settings/_EmailAddresses.scss";
|
||||
@import "./views/settings/_SpellCheckLanguages.scss";
|
||||
@import "./views/settings/_IntegrationManager.scss";
|
||||
@import "./views/settings/_Notifications.scss";
|
||||
@import "./views/settings/_PhoneNumbers.scss";
|
||||
|
@ -232,6 +235,9 @@
|
|||
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||
@import "./views/spaces/_SpaceBasicSettings.scss";
|
||||
@import "./views/spaces/_SpaceCreateMenu.scss";
|
||||
@import "./views/spaces/_SpacePublicShare.scss";
|
||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||
@import "./views/toasts/_AnalyticsToast.scss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
||||
$roomListCollapsedWidth: 68px;
|
||||
|
||||
.mx_LeftPanel {
|
||||
background-color: $roomlist-bg-color;
|
||||
|
@ -37,18 +38,12 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
|
|||
// GroupFilterPanel handles its own CSS
|
||||
}
|
||||
|
||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
||||
.mx_LeftPanel_roomListContainer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The 'room list' in this context is actually everything that isn't the tag
|
||||
// panel, such as the menu options, breadcrumbs, filtering, etc
|
||||
.mx_LeftPanel_roomListContainer {
|
||||
width: calc(100% - $groupFilterPanelWidth);
|
||||
background-color: $roomlist-bg-color;
|
||||
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
// Create another flexbox (this time a column) for the room list components
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -168,17 +163,10 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
|
|||
// These styles override the defaults for the minimized (66px) layout
|
||||
&.mx_LeftPanel_minimized {
|
||||
min-width: unset;
|
||||
|
||||
// We have to forcefully set the width to override the resizer's style attribute.
|
||||
&.mx_LeftPanel_hasGroupFilterPanel {
|
||||
width: calc(68px + $groupFilterPanelWidth) !important;
|
||||
}
|
||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
||||
width: 68px !important;
|
||||
}
|
||||
width: unset !important;
|
||||
|
||||
.mx_LeftPanel_roomListContainer {
|
||||
width: 68px;
|
||||
width: $roomListCollapsedWidth;
|
||||
|
||||
.mx_LeftPanel_userHeader {
|
||||
flex-direction: row;
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
$topLevelHeight: 32px;
|
||||
$nestedHeight: 24px;
|
||||
$gutterSize: 17px;
|
||||
$activeStripeSize: 4px;
|
||||
$activeBorderTransparentGap: 2px;
|
||||
|
||||
$activeBackgroundColor: $roomtile-selected-bg-color;
|
||||
$activeBorderColor: $secondary-fg-color;
|
||||
|
||||
.mx_SpacePanel {
|
||||
flex: 0 0 auto;
|
||||
background-color: $groupFilterPanel-bg-color;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
// Create another flexbox so the Panel fills the container
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
.mx_SpacePanel_spaceTreeWrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_SpacePanel_toggleCollapse {
|
||||
flex: 0 0 auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
mask-position: center;
|
||||
mask-size: 32px;
|
||||
mask-repeat: no-repeat;
|
||||
margin-left: $gutterSize;
|
||||
margin-bottom: 12px;
|
||||
background-color: $roomlist-header-color;
|
||||
mask-image: url('$(res)/img/element-icons/expand-space-panel.svg');
|
||||
|
||||
&.expanded {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.mx_AutoHideScrollbar {
|
||||
padding: 16px 12px 16px 0;
|
||||
}
|
||||
|
||||
.mx_SpaceButton_toggleCollapse {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_SpaceItem.collapsed {
|
||||
.mx_SpaceButton {
|
||||
.mx_NotificationBadge {
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
& > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
& > .mx_SpaceTreeLevel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
|
||||
margin-left: $gutterSize;
|
||||
}
|
||||
|
||||
.mx_SpaceButton {
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
|
||||
&.mx_SpaceButton_active {
|
||||
&:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper {
|
||||
background-color: $activeBackgroundColor;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_narrow {
|
||||
.mx_BaseAvatar, .mx_SpaceButton_avatarPlaceholder {
|
||||
border: 2px $activeBorderColor solid;
|
||||
border-radius: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceButton_selectionWrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_SpaceButton_name {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding-right: 8px;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
.mx_SpaceButton_toggleCollapse {
|
||||
width: calc($gutterSize - $activeStripeSize);
|
||||
margin-left: 1px;
|
||||
height: 20px;
|
||||
mask-position: center;
|
||||
mask-size: 20px;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $roomlist-header-color;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
|
||||
.mx_SpaceButton_icon {
|
||||
width: $topLevelHeight;
|
||||
min-width: $topLevelHeight;
|
||||
height: $topLevelHeight;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: $topLevelHeight;
|
||||
height: $topLevelHeight;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_home .mx_SpaceButton_icon {
|
||||
background-color: #ffffff;
|
||||
|
||||
&::before {
|
||||
background-color: #3f3d3d;
|
||||
mask-image: url('$(res)/img/element-icons/home.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceButton_avatarPlaceholder {
|
||||
border: $activeBorderTransparentGap transparent solid;
|
||||
padding: $activeBorderTransparentGap;
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_new .mx_SpaceButton_icon {
|
||||
background-color: $accent-color;
|
||||
transition: all .1s ease-in-out; // TODO transition
|
||||
|
||||
&::before {
|
||||
background-color: #ffffff;
|
||||
mask-image: url('$(res)/img/element-icons/plus.svg');
|
||||
transition: all .2s ease-in-out; // TODO transition
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_newCancel .mx_SpaceButton_icon {
|
||||
background-color: $icon-button-color;
|
||||
|
||||
&::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BaseAvatar {
|
||||
/* moving the border-radius to this element from _image
|
||||
element so we can add a border to it without the initials being displaced */
|
||||
overflow: hidden;
|
||||
border: 2px transparent solid;
|
||||
padding: $activeBorderTransparentGap;
|
||||
|
||||
.mx_BaseAvatar_initial {
|
||||
top: $activeBorderTransparentGap;
|
||||
left: $activeBorderTransparentGap;
|
||||
}
|
||||
|
||||
.mx_BaseAvatar_image {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpacePanel_badgeContainer {
|
||||
height: 16px;
|
||||
// don't set width so that it takes no space when there is no badge to show
|
||||
margin: auto 0; // vertically align
|
||||
|
||||
// Create a flexbox to make aligning dot badges easier
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mx_NotificationBadge {
|
||||
margin: 0 2px; // centering
|
||||
}
|
||||
|
||||
.mx_NotificationBadge_dot {
|
||||
// make the smaller dot occupy the same width for centering
|
||||
margin-left: 7px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
.mx_SpaceButton {
|
||||
.mx_SpacePanel_badgeContainer {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.collapsed) {
|
||||
.mx_SpaceButton:hover,
|
||||
.mx_SpaceButton:focus-within,
|
||||
.mx_SpaceButton_hasMenuOpen {
|
||||
// Hide the badge container on hover because it'll be a menu button
|
||||
.mx_SpacePanel_badgeContainer {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* root space buttons are bigger and not indented */
|
||||
& > .mx_AutoHideScrollbar {
|
||||
& > .mx_SpaceButton {
|
||||
height: $topLevelHeight;
|
||||
|
||||
&.mx_SpaceButton_active::before {
|
||||
height: $topLevelHeight;
|
||||
}
|
||||
}
|
||||
|
||||
& > ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
$SpaceRoomViewInnerWidth: 428px;
|
||||
|
||||
.mx_SpaceRoomView {
|
||||
.mx_MainSplit > div:first-child {
|
||||
padding: 80px 60px;
|
||||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: $font-24px;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $primary-fg-color;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_description {
|
||||
font-size: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_buttons {
|
||||
display: block;
|
||||
margin-top: 44px;
|
||||
width: $SpaceRoomViewInnerWidth;
|
||||
text-align: right; // button alignment right
|
||||
|
||||
.mx_FormButton {
|
||||
padding: 8px 22px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Field {
|
||||
max-width: $SpaceRoomViewInnerWidth;
|
||||
|
||||
& + .mx_Field {
|
||||
margin-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_errorText {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $notice-primary-color;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing {
|
||||
overflow-y: auto;
|
||||
|
||||
> .mx_BaseAvatar_image,
|
||||
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_name {
|
||||
margin: 24px 0 16px;
|
||||
font-size: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_nameRow {
|
||||
margin-top: 12px;
|
||||
|
||||
> h1 {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_inviter {
|
||||
.mx_BaseAvatar {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_memberCount {
|
||||
position: relative;
|
||||
margin-left: 24px;
|
||||
padding: 0 0 0 28px;
|
||||
line-height: $font-24px;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
background-color: $accent-color;
|
||||
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_topic {
|
||||
font-size: $font-15px;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_joinButtons {
|
||||
margin-top: 24px;
|
||||
|
||||
.mx_FormButton {
|
||||
padding: 8px 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_privateScope {
|
||||
.mx_RadioButton {
|
||||
width: $SpaceRoomViewInnerWidth;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $space-button-outline-color;
|
||||
padding: 16px 16px 16px 72px;
|
||||
margin-top: 36px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
> div:first-of-type {
|
||||
// hide radio dot
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_RadioButton_content {
|
||||
margin: 0;
|
||||
|
||||
> h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: $font-15px;
|
||||
font-weight: $font-semi-bold;
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
> div {
|
||||
color: $secondary-fg-color;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
top: 24px;
|
||||
left: 20px;
|
||||
background-color: $secondary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RadioButton_checked {
|
||||
border-color: $accent-color;
|
||||
|
||||
.mx_RadioButton_content {
|
||||
> div {
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_privateScope_justMeButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/members.svg');
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_inviteTeammates {
|
||||
.mx_SpaceRoomView_inviteTeammates_buttons {
|
||||
color: $secondary-fg-color;
|
||||
margin-top: 28px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-left: 32px;
|
||||
line-height: 24px; // to center icons
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $secondary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
& + .mx_AccessibleButton {
|
||||
margin-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,6 +72,7 @@ limitations under the License.
|
|||
position: relative; // to make default avatars work
|
||||
margin-right: 8px;
|
||||
height: 32px; // to remove the unknown 4px gap the browser puts below it
|
||||
padding: 3px 0; // to align with and without using doubleName
|
||||
|
||||
.mx_UserMenu_userAvatar {
|
||||
border-radius: 32px; // should match avatar size
|
||||
|
|
|
@ -75,6 +75,11 @@ limitations under the License.
|
|||
background-color: $menu-selected-color;
|
||||
}
|
||||
|
||||
&.mx_AccessibleButton_disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
img, .mx_IconizedContextMenu_icon { // icons
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
|
|
|
@ -105,16 +105,9 @@ $left-gutter: 64px;
|
|||
}
|
||||
|
||||
.mx_EventTile_readAvatars {
|
||||
top: 27px;
|
||||
}
|
||||
|
||||
&.mx_EventTile_continuation .mx_EventTile_readAvatars,
|
||||
&.mx_EventTile_emote .mx_EventTile_readAvatars {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.mx_EventTile_info .mx_EventTile_readAvatars {
|
||||
top: 4px;
|
||||
// This aligns the avatar with the last line of the
|
||||
// message. We want to move it one line up - 2rem
|
||||
top: -2rem;
|
||||
}
|
||||
|
||||
.mx_EventTile_content .markdown-body {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ExistingSpellCheckLanguage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_ExistingSpellCheckLanguage_language {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_spellCheckLanguageInput {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_SpellCheckLanguages {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SpaceBasicSettings {
|
||||
.mx_Field {
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.mx_SpaceBasicSettings_avatarContainer {
|
||||
display: flex;
|
||||
margin-top: 24px;
|
||||
|
||||
.mx_SpaceBasicSettings_avatar {
|
||||
position: relative;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
background-color: $tertiary-fg-color;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
img.mx_SpaceBasicSettings_avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
// only show it when the button is a div and not an img (has avatar)
|
||||
div.mx_SpaceBasicSettings_avatar {
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #ffffff; // white icon fill
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 20px;
|
||||
mask-image: url('$(res)/img/element-icons/camera.svg');
|
||||
}
|
||||
}
|
||||
|
||||
> input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .mx_AccessibleButton_kind_link {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: auto 16px;
|
||||
color: #368bd6;
|
||||
}
|
||||
|
||||
> .mx_SpaceBasicSettings_avatar_remove {
|
||||
color: $notice-primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_FormButton {
|
||||
padding: 8px 22px;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: the space panel currently does not have a fixed width,
|
||||
// just the headers at each level have a max-width of 150px
|
||||
// so this will look slightly off for now. We should probably use css grid for the whole main layout...
|
||||
$spacePanelWidth: 200px;
|
||||
|
||||
.mx_SpaceCreateMenu_wrapper {
|
||||
// background blur everything except SpacePanel
|
||||
.mx_ContextualMenu_background {
|
||||
background-color: $dialog-backdrop-color;
|
||||
opacity: 0.6;
|
||||
left: $spacePanelWidth;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu {
|
||||
padding: 24px;
|
||||
width: 480px;
|
||||
box-sizing: border-box;
|
||||
background-color: $primary-bg-color;
|
||||
|
||||
> div {
|
||||
> h2 {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-18px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
> p {
|
||||
font-size: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceCreateMenuType {
|
||||
position: relative;
|
||||
padding: 16px 32px 16px 72px;
|
||||
width: 432px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $input-darker-bg-color;
|
||||
font-size: $font-15px;
|
||||
margin: 20px 0;
|
||||
|
||||
> h3 {
|
||||
font-weight: $font-semi-bold;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
> span {
|
||||
color: $secondary-fg-color;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
top: 24px;
|
||||
left: 20px;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 32px;
|
||||
background-color: $tertiary-fg-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $accent-color;
|
||||
|
||||
&::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
> span {
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceCreateMenuType_public::before {
|
||||
mask-image: url('$(res)/img/globe.svg');
|
||||
mask-size: 26px;
|
||||
}
|
||||
.mx_SpaceCreateMenuType_private::before {
|
||||
mask-image: url('$(res)/img/element-icons/lock.svg');
|
||||
}
|
||||
|
||||
.mx_SpaceCreateMenu_back {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
background-color: $theme-button-bg-color;
|
||||
border-radius: 14px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $muted-fg-color;
|
||||
transform: rotate(90deg);
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 2px 3px;
|
||||
mask-size: 24px;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_FormButton {
|
||||
padding: 8px 22px;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SpacePublicShare {
|
||||
.mx_AccessibleButton {
|
||||
border: 1px solid $space-button-outline-color;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px 12px 52px;
|
||||
margin-top: 16px;
|
||||
width: $SpaceRoomViewInnerWidth;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
> span {
|
||||
color: #368bd6;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(141, 151, 165, 0.1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-position: center;
|
||||
background: $muted-fg-color;
|
||||
left: 12px;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
&.mx_SpacePublicShare_shareButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/link.svg');
|
||||
}
|
||||
|
||||
&.mx_SpacePublicShare_inviteButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7701 16.617H22.3721L18.614 20.3751C18.3137 20.6754 18.3137 21.1683 18.614 21.4686C18.9143 21.769 19.3995 21.769 19.6998 21.4686L24.7747 16.3937C25.0751 16.0934 25.0751 15.6082 24.7747 15.3079L19.7075 10.2253C19.4072 9.92492 18.922 9.92492 18.6217 10.2253C18.3214 10.5256 18.3214 11.0107 18.6217 11.3111L22.3721 15.0768H13.7701C13.3465 15.0768 13 15.4234 13 15.8469C13 16.2705 13.3465 16.617 13.7701 16.617Z" fill="#86888A"/>
|
||||
<rect x="7" y="10" width="1.5" height="12" rx="0.75" fill="#86888A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 651 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5285 6.54089L13.0273 6.04207C14.4052 4.66426 16.6259 4.65104 17.9874 6.01253C19.349 7.37402 19.3357 9.59466 17.9579 10.9725L15.5878 13.3425C14.21 14.7203 11.9893 14.7335 10.6277 13.372M11.4717 17.4589L10.9727 17.9579C9.59481 19.3357 7.37409 19.349 6.01256 17.9875C4.65102 16.626 4.66426 14.4053 6.04211 13.0275L8.41203 10.6577C9.78988 9.27988 12.0106 9.26665 13.3721 10.6281" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 549 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1113 2.6665C11.1839 2.6665 8.00016 5.85026 8.00016 9.77762V13.3332L7.3335 13.3332C6.22893 13.3332 5.3335 14.2286 5.3335 15.3332V27.3332C5.3335 28.4377 6.22893 29.3332 7.3335 29.3332H24.6668C25.7714 29.3332 26.6668 28.4377 26.6668 27.3332V15.3332C26.6668 14.2286 25.7714 13.3332 24.6668 13.3332L24.0002 13.3332V9.77762C24.0002 5.85026 20.8164 2.6665 16.8891 2.6665H15.1113ZM20.4446 13.3332V9.77762C20.4446 7.81394 18.8527 6.22206 16.8891 6.22206H15.1113C13.1476 6.22206 11.5557 7.81394 11.5557 9.77762V13.3332H20.4446Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 692 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.74986 3.55554C8.74986 3.14133 8.41408 2.80554 7.99986 2.80554C7.58565 2.80554 7.24986 3.14133 7.24986 3.55554V7.24999L3.55542 7.24999C3.14121 7.24999 2.80542 7.58577 2.80542 7.99999C2.80542 8.4142 3.14121 8.74999 3.55542 8.74999L7.24987 8.74999V12.4444C7.24987 12.8586 7.58565 13.1944 7.99987 13.1944C8.41408 13.1944 8.74987 12.8586 8.74987 12.4444V8.74999L12.4443 8.74999C12.8585 8.74999 13.1943 8.4142 13.1943 7.99999C13.1943 7.58577 12.8585 7.24999 12.4443 7.24999L8.74986 7.24999V3.55554Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 670 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="24" height="24" viewBox="-0.4 1 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.1001 9C18.7779 9 18.5168 8.73883 18.5168 8.41667V6.08333H16.1834C15.8613 6.08333 15.6001 5.82217 15.6001 5.5C15.6001 5.17783 15.8613 4.91667 16.1834 4.91667H18.5168V2.58333C18.5168 2.26117 18.7779 2 19.1001 2C19.4223 2 19.6834 2.26117 19.6834 2.58333V4.91667H22.0168C22.3389 4.91667 22.6001 5.17783 22.6001 5.5C22.6001 5.82217 22.3389 6.08333 22.0168 6.08333H19.6834V8.41667C19.6834 8.73883 19.4223 9 19.1001 9ZM19.6001 11C20.0669 11 20.5212 10.9467 20.9574 10.8458C21.1161 11.5383 21.2 12.2594 21.2 13C21.2 16.1409 19.6917 18.9294 17.3598 20.6808V20.6807C16.0014 21.7011 14.3635 22.3695 12.5815 22.5505C12.2588 22.5832 11.9314 22.6 11.6 22.6C6.29807 22.6 2 18.302 2 13C2 7.69809 6.29807 3.40002 11.6 3.40002C12.3407 3.40002 13.0618 3.48391 13.7543 3.64268C13.6534 4.07884 13.6001 4.53319 13.6001 5C13.6001 8.31371 16.2864 11 19.6001 11ZM11.5999 20.68C13.6754 20.68 15.5585 19.8567 16.9407 18.5189C16.0859 16.4086 14.0167 14.92 11.5998 14.92C9.18298 14.92 7.11378 16.4086 6.25901 18.5189C7.64115 19.8567 9.52436 20.68 11.5999 20.68ZM11.7426 7.41172C10.3168 7.54168 9.2 8.74043 9.2 10.2C9.2 11.7464 10.4536 13 12 13C13.0308 13 13.9315 12.443 14.4176 11.6135C13.0673 10.6058 12.0929 9.12248 11.7426 7.41172Z" fill="black"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -123,6 +123,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: rgba(141, 151, 165, 0.2);
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -120,6 +120,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: rgba(141, 151, 165, 0.2);
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -187,6 +187,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: #E3E8F0;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -181,6 +181,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: #E3E8F0;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
||||
}
|
||||
|
||||
.mx_SpacePanel {
|
||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
||||
}
|
||||
|
||||
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
||||
backdrop-filter: blur($roomlist-background-blur-amount);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import UserActivity from "../UserActivity";
|
|||
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
import VoipUserMapper from "../VoipUserMapper";
|
||||
import {SpaceStoreClass} from "../stores/SpaceStore";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -68,6 +69,7 @@ declare global {
|
|||
mxUserActivity: UserActivity;
|
||||
mxModalWidgetStore: ModalWidgetStore;
|
||||
mxVoipUserMapper: VoipUserMapper;
|
||||
mxSpaceStore: SpaceStoreClass;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
|
|
|
@ -131,6 +131,14 @@ export default abstract class BasePlatform {
|
|||
hideUpdateToast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if platform supports multi-language
|
||||
* spell-checking, otherwise false.
|
||||
*/
|
||||
supportsMultiLanguageSpellCheck(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the platform supports displaying
|
||||
* notifications, otherwise false.
|
||||
|
@ -240,6 +248,16 @@ export default abstract class BasePlatform {
|
|||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
setSpellCheckLanguages(preferredLangs: string[]) {}
|
||||
|
||||
getSpellCheckLanguages(): Promise<string[]> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getAvailableSpellCheckLanguages(): Promise<string[]> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = fragmentAfterLogin || "";
|
||||
|
|
|
@ -629,6 +629,8 @@ export default class CallHandler {
|
|||
const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
|
||||
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
|
||||
|
||||
const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
|
||||
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds");
|
||||
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
|
||||
|
||||
this.calls.set(roomId, call);
|
||||
|
|
|
@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
|
|||
import Modal from './Modal';
|
||||
import * as sdk from './';
|
||||
import { _t } from './languageHandler';
|
||||
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||
|
||||
|
@ -75,6 +75,13 @@ export function showCommunityInviteDialog(communityId) {
|
|||
}
|
||||
}
|
||||
|
||||
export const showSpaceInviteDialog = (roomId) => {
|
||||
Modal.createTrackedDialog("Invite Users", "Space", InviteDialog, {
|
||||
kind: KIND_SPACE_INVITE,
|
||||
roomId,
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given MatrixEvent is a valid 3rd party user invite.
|
||||
* @param {MatrixEvent} event The event to check
|
||||
|
|
|
@ -76,6 +76,7 @@ export interface IProps extends IPosition {
|
|||
hasBackground?: boolean;
|
||||
// whether this context menu should be focus managed. If false it must handle itself
|
||||
managed?: boolean;
|
||||
wrapperClassName?: string;
|
||||
|
||||
// Function to be called on menu close
|
||||
onFinished();
|
||||
|
@ -365,7 +366,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="mx_ContextualMenu_wrapper"
|
||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
style={{...position, ...wrapperStyle}}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
|
|
|
@ -39,6 +39,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
|||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||
import LeftPanelWidget from "./LeftPanelWidget";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -388,12 +389,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const groupFilterPanel = !this.state.showGroupFilterPanel ? null : (
|
||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
||||
<GroupFilterPanel />
|
||||
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
|
||||
</div>
|
||||
);
|
||||
let leftLeftPanel;
|
||||
// Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now
|
||||
// ignore it and force the rendering of SpacePanel if that Labs flag is enabled.
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
leftLeftPanel = <SpacePanel />;
|
||||
} else if (this.state.showGroupFilterPanel) {
|
||||
leftLeftPanel = (
|
||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
||||
<GroupFilterPanel />
|
||||
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const roomList = <RoomList
|
||||
onKeyDown={this.onKeyDown}
|
||||
|
@ -406,7 +414,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
const containerClasses = classNames({
|
||||
"mx_LeftPanel": true,
|
||||
"mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel,
|
||||
"mx_LeftPanel_minimized": this.props.isMinimized,
|
||||
});
|
||||
|
||||
|
@ -417,7 +424,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{groupFilterPanel}
|
||||
{leftLeftPanel}
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
{this.renderHeader()}
|
||||
{this.renderSearchExplore()}
|
||||
|
|
|
@ -55,6 +55,7 @@ import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
|||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||
import { IOpts } from "../../createRoom";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
|
@ -91,6 +92,7 @@ interface IProps {
|
|||
currentGroupId?: string;
|
||||
currentGroupIsNew?: boolean;
|
||||
justRegistered?: boolean;
|
||||
roomJustCreatedOpts?: IOpts;
|
||||
}
|
||||
|
||||
interface IUsageLimit {
|
||||
|
@ -619,6 +621,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
viaServers={this.props.viaServers}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
justCreatedOpts={this.props.roomJustCreatedOpts}
|
||||
/>;
|
||||
break;
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ import * as Lifecycle from '../../Lifecycle';
|
|||
import '../../stores/LifecycleStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import createRoom, {IOpts} from "../../createRoom";
|
||||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
|
@ -144,6 +144,8 @@ interface IRoomInfo {
|
|||
oob_data?: object;
|
||||
via_servers?: string[];
|
||||
threepid_invite?: IThreepidInvite;
|
||||
|
||||
justCreatedOpts?: IOpts;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
@ -201,6 +203,7 @@ interface IState {
|
|||
viaServers?: string[];
|
||||
pendingInitialSync?: boolean;
|
||||
justRegistered?: boolean;
|
||||
roomJustCreatedOpts?: IOpts;
|
||||
}
|
||||
|
||||
export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
|
@ -922,6 +925,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
roomOobData: roomInfo.oob_data,
|
||||
viaServers: roomInfo.via_servers,
|
||||
ready: true,
|
||||
roomJustCreatedOpts: roomInfo.justCreatedOpts,
|
||||
}, () => {
|
||||
this.notifyNewScreen('room/' + presentedId, replaceLast);
|
||||
});
|
||||
|
|
|
@ -24,7 +24,11 @@ import dis from '../../dispatcher/dispatcher';
|
|||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
|
||||
import {
|
||||
RightPanelPhases,
|
||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||
RIGHT_PANEL_SPACE_PHASES,
|
||||
} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
@ -79,6 +83,8 @@ export default class RightPanel extends React.Component {
|
|||
return RightPanelPhases.GroupMemberList;
|
||||
}
|
||||
return rps.groupPanelPhase;
|
||||
} else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
|
||||
return RightPanelPhases.SpaceMemberList;
|
||||
} else if (userForPanel) {
|
||||
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
||||
// from its props and some from a store, except if the contents of the store changes
|
||||
|
@ -99,9 +105,8 @@ export default class RightPanel extends React.Component {
|
|||
return rps.roomPanelPhase;
|
||||
}
|
||||
return RightPanelPhases.RoomMemberInfo;
|
||||
} else {
|
||||
return rps.roomPanelPhase;
|
||||
}
|
||||
return rps.roomPanelPhase;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -181,6 +186,7 @@ export default class RightPanel extends React.Component {
|
|||
verificationRequest: payload.verificationRequest,
|
||||
verificationRequestPromise: payload.verificationRequestPromise,
|
||||
widgetId: payload.widgetId,
|
||||
space: payload.space,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -232,6 +238,13 @@ export default class RightPanel extends React.Component {
|
|||
panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
|
||||
}
|
||||
break;
|
||||
case RightPanelPhases.SpaceMemberList:
|
||||
panel = <MemberList
|
||||
roomId={this.state.space ? this.state.space.roomId : roomId}
|
||||
key={this.state.space ? this.state.space.roomId : roomId}
|
||||
onClose={this.onClose}
|
||||
/>;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupMemberList:
|
||||
if (this.props.groupId) {
|
||||
|
@ -244,10 +257,11 @@ export default class RightPanel extends React.Component {
|
|||
break;
|
||||
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
case RightPanelPhases.SpaceMemberInfo:
|
||||
case RightPanelPhases.EncryptionPanel:
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
room={this.props.room}
|
||||
room={this.state.phase === RightPanelPhases.SpaceMemberInfo ? this.state.space : this.props.room}
|
||||
key={roomId || this.state.member.userId}
|
||||
onClose={this.onClose}
|
||||
phase={this.state.phase}
|
||||
|
@ -257,6 +271,7 @@ export default class RightPanel extends React.Component {
|
|||
break;
|
||||
|
||||
case RightPanelPhases.Room3pidMemberInfo:
|
||||
case RightPanelPhases.Space3pidMemberInfo:
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={roomId} />;
|
||||
break;
|
||||
|
||||
|
|
|
@ -80,6 +80,8 @@ import { showToast as showNotificationsToast } from "../../toasts/DesktopNotific
|
|||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { objectHasDiff } from "../../utils/objects";
|
||||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -114,6 +116,7 @@ interface IProps {
|
|||
|
||||
autoJoin?: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
justCreatedOpts?: IOpts;
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
|
||||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
|
@ -1397,7 +1400,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onRejectButtonClicked = ev => {
|
||||
private onRejectButtonClicked = () => {
|
||||
this.setState({
|
||||
rejecting: true,
|
||||
});
|
||||
|
@ -1457,7 +1460,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRejectThreepidInviteButtonClicked = ev => {
|
||||
private onRejectThreepidInviteButtonClicked = () => {
|
||||
// We can reject 3pid invites in the same way that we accept them,
|
||||
// using /leave rather than /join. In the short term though, we
|
||||
// just ignore them.
|
||||
|
@ -1720,7 +1723,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const myMembership = this.state.room.getMyMembership();
|
||||
if (myMembership == 'invite') {
|
||||
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
|
@ -1849,7 +1852,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
room={this.state.room}
|
||||
/>
|
||||
);
|
||||
if (!this.state.canPeek) {
|
||||
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
{ previewBar }
|
||||
|
@ -1871,6 +1874,18 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.room?.isSpaceRoom()) {
|
||||
return <SpaceRoomView
|
||||
space={this.state.room}
|
||||
justCreatedOpts={this.props.justCreatedOpts}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
onJoinButtonClicked={this.onJoinButtonClicked}
|
||||
onRejectButtonClicked={this.props.threepidInvite
|
||||
? this.onRejectThreepidInviteButtonClicked
|
||||
: this.onRejectButtonClicked}
|
||||
/>;
|
||||
}
|
||||
|
||||
const auxPanel = (
|
||||
<AuxPanel
|
||||
room={this.state.room}
|
||||
|
|
|
@ -0,0 +1,503 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {RefObject, useContext, useRef, useState} from "react";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import RoomTopic from "../views/elements/RoomTopic";
|
||||
import FormButton from "../views/elements/FormButton";
|
||||
import {inviteMultipleToRoom, showSpaceInviteDialog} from "../../RoomInvite";
|
||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import StyledRadioGroup from "../views/elements/StyledRadioGroup";
|
||||
import withValidation from "../views/elements/Validation";
|
||||
import * as Email from "../../email";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier"
|
||||
import MainSplit from './MainSplit';
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import {ActionPayload} from "../../dispatcher/payloads";
|
||||
import RightPanel from "./RightPanel";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {useStateArray} from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {shouldShowSpaceSettings} from "../../utils/space";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
justCreatedOpts?: IOpts;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
onJoinButtonClicked(): void;
|
||||
onRejectButtonClicked(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
showRightPanel: boolean;
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
Landing,
|
||||
PublicCreateRooms,
|
||||
PublicShare,
|
||||
PrivateScope,
|
||||
PrivateInvite,
|
||||
PrivateCreateRooms,
|
||||
PrivateExistingRooms,
|
||||
}
|
||||
|
||||
const RoomMemberCount = ({ room, children }) => {
|
||||
const members = useRoomMembers(room);
|
||||
const count = members.length;
|
||||
|
||||
if (children) return children(count);
|
||||
return count;
|
||||
};
|
||||
|
||||
const useMyRoomMembership = (room: Room) => {
|
||||
const [membership, setMembership] = useState(room.getMyMembership());
|
||||
useEventEmitter(room, "Room.myMembership", () => {
|
||||
setMembership(room.getMyMembership());
|
||||
});
|
||||
return membership;
|
||||
};
|
||||
|
||||
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
const joinRule = space.getJoinRule();
|
||||
const userId = cli.getUserId();
|
||||
|
||||
let joinButtons;
|
||||
if (myMembership === "invite") {
|
||||
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
|
||||
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
|
||||
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
|
||||
{_t("Decline")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (myMembership !== "join" && joinRule === "public") {
|
||||
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
|
||||
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_landing">
|
||||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||
<div className="mx_SpaceRoomView_landing_name">
|
||||
<RoomName room={space}>
|
||||
{(name) => {
|
||||
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
|
||||
<h1>{ name }</h1>
|
||||
<RoomMemberCount room={space}>
|
||||
{(count) => count > 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_landing_memberCount"
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.RoomMemberList,
|
||||
refireParams: { space },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("%(count)s members", { count }) }
|
||||
</AccessibleButton>
|
||||
) : null}
|
||||
</RoomMemberCount>
|
||||
</div> };
|
||||
if (myMembership === "invite") {
|
||||
const inviteSender = space.getMember(userId)?.events.member?.getSender();
|
||||
const inviter = inviteSender && space.getMember(inviteSender);
|
||||
|
||||
if (inviteSender) {
|
||||
return _t("<inviter/> invited you to <name/>", {}, {
|
||||
name: tags.name,
|
||||
inviter: () => inviter
|
||||
? <span className="mx_SpaceRoomView_landing_inviter">
|
||||
<MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
|
||||
{ inviter.name }
|
||||
</span>
|
||||
: <span className="mx_SpaceRoomView_landing_inviter">
|
||||
{ inviteSender }
|
||||
</span>,
|
||||
}) as JSX.Element;
|
||||
} else {
|
||||
return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
|
||||
}
|
||||
} else if (shouldShowSpaceSettings(cli, space)) {
|
||||
if (space.getJoinRule() === "public") {
|
||||
return _t("Your public space <name/>", {}, tags) as JSX.Element;
|
||||
} else {
|
||||
return _t("Your private space <name/>", {}, tags) as JSX.Element;
|
||||
}
|
||||
}
|
||||
return _t("Welcome to <name/>", {}, tags) as JSX.Element;
|
||||
}}
|
||||
</RoomName>
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_topic">
|
||||
<RoomTopic room={space} />
|
||||
</div>
|
||||
{ joinButtons }
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const numFields = 3;
|
||||
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
||||
// TODO vary default prefills for "Just Me" spaces
|
||||
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
||||
const name = "roomName" + i;
|
||||
return <Field
|
||||
key={name}
|
||||
name={name}
|
||||
type="text"
|
||||
label={_t("Room name")}
|
||||
placeholder={placeholders[i]}
|
||||
value={roomNames[i]}
|
||||
onChange={ev => setRoomName(i, ev.target.value)}
|
||||
/>;
|
||||
});
|
||||
|
||||
const onNextClick = async () => {
|
||||
setError("");
|
||||
setBusy(true);
|
||||
try {
|
||||
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => {
|
||||
return createRoom({
|
||||
createOpts: {
|
||||
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
},
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: false,
|
||||
inlineErrors: true,
|
||||
parentSpace: space,
|
||||
});
|
||||
}));
|
||||
onFinished();
|
||||
} catch (e) {
|
||||
console.error("Failed to create initial space rooms", e);
|
||||
setError(_t("Failed to create initial space rooms"));
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let onClick = onFinished;
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (roomNames.some(name => name.trim())) {
|
||||
onClick = onNextClick;
|
||||
buttonLabel = busy ? _t("Creating rooms...") : _t("Next")
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h1>{ title }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ description }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
{ fields }
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton
|
||||
label={buttonLabel}
|
||||
disabled={busy}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupPublicShare = ({ space, onFinished }) => {
|
||||
return <div className="mx_SpaceRoomView_publicShare">
|
||||
<h1>{ _t("Share your public space") }</h1>
|
||||
<div className="mx_SpacePublicShare_description">{ _t("At the moment only you can see it.") }</div>
|
||||
|
||||
<SpacePublicShare space={space} onFinished={onFinished} />
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton label={_t("Finish")} onClick={onFinished} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupPrivateScope = ({ onFinished }) => {
|
||||
const [option, setOption] = useState<string>(null);
|
||||
|
||||
return <div className="mx_SpaceRoomView_privateScope">
|
||||
<h1>{ _t("Who are you working with?") }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
|
||||
|
||||
<StyledRadioGroup
|
||||
name="privateSpaceScope"
|
||||
value={option}
|
||||
onChange={setOption}
|
||||
definitions={[
|
||||
{
|
||||
value: "justMe",
|
||||
className: "mx_SpaceRoomView_privateScope_justMeButton",
|
||||
label: <React.Fragment>
|
||||
<h3>{ _t("Just Me") }</h3>
|
||||
<div>{ _t("A private space just for you") }</div>
|
||||
</React.Fragment>,
|
||||
}, {
|
||||
value: "meAndMyTeammates",
|
||||
className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton",
|
||||
label: <React.Fragment>
|
||||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</React.Fragment>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton label={_t("Next")} disabled={!option} onClick={() => onFinished(option !== "justMe")} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const validateEmailRules = withValidation({
|
||||
rules: [{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
}],
|
||||
});
|
||||
|
||||
const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const numFields = 3;
|
||||
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
|
||||
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
|
||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
||||
const name = "emailAddress" + i;
|
||||
return <Field
|
||||
key={name}
|
||||
name={name}
|
||||
type="text"
|
||||
label={_t("Email address")}
|
||||
placeholder={_t("Email")}
|
||||
value={emailAddresses[i]}
|
||||
onChange={ev => setEmailAddress(i, ev.target.value)}
|
||||
ref={fieldRefs[i]}
|
||||
onValidate={validateEmailRules}
|
||||
/>;
|
||||
});
|
||||
|
||||
const onNextClick = async () => {
|
||||
setError("");
|
||||
for (let i = 0; i < fieldRefs.length; i++) {
|
||||
const fieldRef = fieldRefs[i];
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: true });
|
||||
|
||||
if (valid === false) { // true/null are allowed
|
||||
fieldRef.current.focus();
|
||||
fieldRef.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
const targetIds = emailAddresses.map(name => name.trim()).filter(Boolean);
|
||||
try {
|
||||
const result = await inviteMultipleToRoom(space.roomId, targetIds);
|
||||
|
||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error");
|
||||
if (failedUsers.length > 0) {
|
||||
console.log("Failed to invite users to space: ", result);
|
||||
setError(_t("Failed to invite the following users to your space: %(csvUsers)s", {
|
||||
csvUsers: failedUsers.join(", "),
|
||||
}));
|
||||
} else {
|
||||
onFinished();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to invite users to space: ", err);
|
||||
setError(_t("We couldn't invite those users. Please check the users you want to invite and try again."));
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
return <div className="mx_SpaceRoomView_inviteTeammates">
|
||||
<h1>{ _t("Invite your teammates") }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
{ fields }
|
||||
|
||||
<div className="mx_SpaceRoomView_inviteTeammates_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_inviteTeammates_inviteDialogButton"
|
||||
onClick={() => showSpaceInviteDialog(space.roomId)}
|
||||
>
|
||||
{ _t("Invite by username") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<AccessibleButton onClick={onFinished} kind="link">{_t("Skip for now")}</AccessibleButton>
|
||||
<FormButton label={busy ? _t("Inviting...") : _t("Next")} disabled={busy} onClick={onNextClick} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private readonly creator: string;
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
let phase = Phase.Landing;
|
||||
|
||||
this.creator = this.props.space.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
const showSetup = this.props.justCreatedOpts && this.context.getUserId() === this.creator;
|
||||
|
||||
if (showSetup) {
|
||||
phase = this.props.justCreatedOpts.createOpts.preset === Preset.PublicChat
|
||||
? Phase.PublicCreateRooms : Phase.PrivateScope;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
phase,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.rightPanelStoreToken.remove();
|
||||
}
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
});
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return;
|
||||
|
||||
if (payload.action === Action.ViewUser && payload.member) {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberInfo,
|
||||
refireParams: {
|
||||
space: this.props.space,
|
||||
member: payload.member,
|
||||
},
|
||||
});
|
||||
} else if (payload.action === "view_3pid_invite" && payload.event) {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.Space3pidMemberInfo,
|
||||
refireParams: {
|
||||
space: this.props.space,
|
||||
event: payload.event,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberList,
|
||||
refireParams: { space: this.props.space },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private renderBody() {
|
||||
switch (this.state.phase) {
|
||||
case Phase.Landing:
|
||||
return <SpaceLanding
|
||||
space={this.props.space}
|
||||
onJoinButtonClicked={this.props.onJoinButtonClicked}
|
||||
onRejectButtonClicked={this.props.onRejectButtonClicked}
|
||||
/>;
|
||||
|
||||
case Phase.PublicCreateRooms:
|
||||
return <SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("What discussions do you want to have?")}
|
||||
description={_t("We'll create rooms for each topic.")}
|
||||
onFinished={() => this.setState({ phase: Phase.PublicShare })}
|
||||
/>;
|
||||
case Phase.PublicShare:
|
||||
return <SpaceSetupPublicShare
|
||||
space={this.props.space}
|
||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||
/>;
|
||||
|
||||
case Phase.PrivateScope:
|
||||
return <SpaceSetupPrivateScope
|
||||
onFinished={(invite: boolean) => {
|
||||
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
|
||||
}}
|
||||
/>;
|
||||
case Phase.PrivateInvite:
|
||||
return <SpaceSetupPrivateInvite
|
||||
space={this.props.space}
|
||||
onFinished={() => this.setState({ phase: Phase.PrivateCreateRooms })}
|
||||
/>;
|
||||
case Phase.PrivateCreateRooms:
|
||||
return <SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("What projects are you working on?")}
|
||||
description={_t("We'll create rooms for each of them. You can add existing rooms after setup.")}
|
||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const rightPanel = this.state.showRightPanel && this.state.phase === Phase.Landing
|
||||
? <RightPanel room={this.props.space} resizeNotifier={this.props.resizeNotifier} />
|
||||
: null;
|
||||
|
||||
return <main className="mx_SpaceRoomView">
|
||||
<ErrorBoundary>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
{ this.renderBody() }
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
</main>;
|
||||
}
|
||||
}
|
|
@ -15,13 +15,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ContextMenuButton } from "./ContextMenu";
|
||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
import Modal from "../../Modal";
|
||||
|
@ -30,11 +35,10 @@ import SettingsStore from "../../settings/SettingsStore";
|
|||
import {getCustomTheme} from "../../theme";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import { getHomePageUrl } from "../../utils/pages";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import classNames from "classnames";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import IconizedContextMenu, {
|
||||
|
@ -42,16 +46,16 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||
import { showCommunityInviteDialog } from "../../RoomInvite";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import { UIFeature } from "../../settings/UIFeature";
|
||||
import HostSignupAction from "./HostSignupAction";
|
||||
import {IHostSignupConfig} from "../views/dialogs/HostSignupDialogTypes";
|
||||
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -62,6 +66,7 @@ type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
|
|||
interface IState {
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
isDarkTheme: boolean;
|
||||
selectedSpace?: Room;
|
||||
}
|
||||
|
||||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
|
@ -79,6 +84,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
|
@ -96,6 +104,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
this.tagStoreRef.remove();
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
|
@ -120,6 +131,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onSelectedSpaceUpdate = async (selectedSpace?: Room) => {
|
||||
this.setState({ selectedSpace });
|
||||
};
|
||||
|
||||
private onThemeChanged = () => {
|
||||
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
|
||||
};
|
||||
|
@ -517,7 +532,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
{/* masked image in CSS */}
|
||||
</span>
|
||||
);
|
||||
if (prototypeCommunityName) {
|
||||
if (this.state.selectedSpace) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{displayName}</span>
|
||||
<RoomName room={this.state.selectedSpace}>
|
||||
{(roomName) => <span className="mx_UserMenu_subUserName">{roomName}</span>}
|
||||
</RoomName>
|
||||
</div>
|
||||
);
|
||||
} else if (prototypeCommunityName) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{prototypeCommunityName}</span>
|
||||
|
|
|
@ -24,7 +24,7 @@ import Modal from '../../../Modal';
|
|||
import * as Avatar from '../../../Avatar';
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick">{
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
|
@ -48,6 +48,7 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_SPACE_INVITE = "space_invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
|
@ -309,7 +310,7 @@ interface IInviteDialogProps {
|
|||
// not provided.
|
||||
kind: string,
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
|
||||
roomId: string,
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
|
@ -348,8 +349,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.kind === KIND_INVITE && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
||||
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
|
||||
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
|
||||
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
||||
}
|
||||
|
@ -1026,7 +1027,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
|
||||
}
|
||||
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||
}
|
||||
|
||||
|
@ -1247,38 +1248,35 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
title = _t("Invite to this room");
|
||||
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
|
||||
|
||||
if (identityServersEnabled) {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, email address, username (like <userId/>) or " +
|
||||
"<a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
let helpTextUntranslated;
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
if (identityServersEnabled) {
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
} else {
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
}
|
||||
} else { // KIND_SPACE_INVITE
|
||||
if (identityServersEnabled) {
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
} else {
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
}
|
||||
}
|
||||
|
||||
helpText = _t(helpTextUntranslated, {}, {
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
});
|
||||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
|
|
|
@ -100,10 +100,10 @@ export default class LanguageDropdown extends React.Component {
|
|||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
return <Dropdown
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Dropdown from "../../views/elements/Dropdown"
|
||||
import * as sdk from '../../../index';
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||
if (language.value.toUpperCase() === query.toUpperCase()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIProps {
|
||||
className: string,
|
||||
value: string,
|
||||
onOptionChange(language: string),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIState {
|
||||
searchQuery: string,
|
||||
languages: any,
|
||||
}
|
||||
|
||||
export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
|
||||
SpellCheckLanguagesDropdownIState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
languages: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
plaf.getAvailableSpellCheckLanguages().then((languages) => {
|
||||
languages.sort(function(a, b) {
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
});
|
||||
const langs = [];
|
||||
languages.forEach((language) => {
|
||||
langs.push({
|
||||
label: language,
|
||||
value: language,
|
||||
})
|
||||
})
|
||||
this.setState({languages: langs});
|
||||
}).catch((e) => {
|
||||
this.setState({languages: ['en']});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.languages === null) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let displayedLanguages;
|
||||
if (this.state.searchQuery) {
|
||||
displayedLanguages = this.state.languages.filter((lang) => {
|
||||
return languageMatchesSearchQuery(this.state.searchQuery, lang);
|
||||
});
|
||||
} else {
|
||||
displayedLanguages = this.state.languages;
|
||||
}
|
||||
|
||||
const options = displayedLanguages.map((language) => {
|
||||
return <div key={language.value}>
|
||||
{ language.label }
|
||||
</div>;
|
||||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined;
|
||||
// values between mounting and the initial value propgating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
return <Dropdown
|
||||
id="mx_LanguageDropdown"
|
||||
className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
searchEnabled={true}
|
||||
value={value}
|
||||
label={_t("Language Dropdown")}>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
|
||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
interface ExistingSpellCheckLanguageIProps {
|
||||
language: string,
|
||||
onRemoved(language: string),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIProps {
|
||||
languages: Array<string>,
|
||||
onLanguagesChange(languages: Array<string>),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIState {
|
||||
newLanguage: string,
|
||||
}
|
||||
|
||||
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||
_onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
return this.props.onRemoved(this.props.language);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_ExistingSpellCheckLanguage">
|
||||
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
newLanguage: "",
|
||||
}
|
||||
}
|
||||
|
||||
_onRemoved = (language) => {
|
||||
const languages = this.props.languages.filter((e) => e !== language);
|
||||
this.props.onLanguagesChange(languages);
|
||||
};
|
||||
|
||||
_onAddClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const language = this.state.newLanguage;
|
||||
|
||||
if (!language) return;
|
||||
if (this.props.languages.includes(language)) return;
|
||||
|
||||
this.props.languages.push(language)
|
||||
this.props.onLanguagesChange(this.props.languages);
|
||||
};
|
||||
|
||||
_onNewLanguageChange = (language: string) => {
|
||||
if (this.state.newLanguage === language) return;
|
||||
this.setState({newLanguage: language});
|
||||
};
|
||||
|
||||
render() {
|
||||
const existingSpellCheckLanguages = this.props.languages.map((e) => {
|
||||
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
|
||||
});
|
||||
|
||||
const addButton = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_SpellCheckLanguages">
|
||||
{existingSpellCheckLanguages}
|
||||
<form onSubmit={this._onAddClick} noValidate={true}>
|
||||
<SpellCheckLanguagesDropdown
|
||||
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
value={this.state.newLanguage}
|
||||
onOptionChange={this._onNewLanguageChange} />
|
||||
{addButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import ProfileSettings from "../../ProfileSettings";
|
|||
import * as languageHandler from "../../../../../languageHandler";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||
import PropTypes from "prop-types";
|
||||
|
@ -49,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
this.state = {
|
||||
language: languageHandler.getCurrentLanguage(),
|
||||
spellCheckLanguages: [],
|
||||
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
|
||||
serverSupportsSeparateAddAndBind: null,
|
||||
idServerHasUnsignedTerms: false,
|
||||
|
@ -85,6 +87,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
this._getThreepidState();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
this.setState({
|
||||
spellCheckLanguages: await plaf.getSpellCheckLanguages(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
@ -182,6 +193,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
PlatformPeg.get().reload();
|
||||
};
|
||||
|
||||
_onSpellCheckLanguagesChange = (languages) => {
|
||||
this.setState({spellCheckLanguages: languages});
|
||||
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
plaf.setSpellCheckLanguages(languages);
|
||||
}
|
||||
};
|
||||
|
||||
_onPasswordChangeError = (err) => {
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
let errMsg = err.error || "";
|
||||
|
@ -303,6 +323,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderSpellCheckSection() {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
|
||||
<SpellCheckSettings languages={this.state.spellCheckLanguages}
|
||||
onLanguagesChange={this._onSpellCheckLanguagesChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderDiscoverySection() {
|
||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
||||
|
||||
|
@ -381,6 +411,9 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const plaf = PlatformPeg.get();
|
||||
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
||||
|
||||
const discoWarning = this.state.requiredPolicyInfo.hasTerms
|
||||
? <img className='mx_GeneralUserSettingsTab_warningIcon'
|
||||
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
|
||||
|
@ -409,6 +442,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
{this._renderProfileSection()}
|
||||
{this._renderAccountSection()}
|
||||
{this._renderLanguageSection()}
|
||||
{supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
|
||||
{ discoverySection }
|
||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||
{ accountManagementSection }
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useRef, useState} from "react";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
|
||||
interface IProps {
|
||||
avatarUrl?: string;
|
||||
avatarDisabled?: boolean;
|
||||
name?: string,
|
||||
nameDisabled?: boolean;
|
||||
topic?: string;
|
||||
topicDisabled?: boolean;
|
||||
setAvatar(avatar: File): void;
|
||||
setName(name: string): void;
|
||||
setTopic(topic: string): void;
|
||||
}
|
||||
|
||||
const SpaceBasicSettings = ({
|
||||
avatarUrl,
|
||||
avatarDisabled = false,
|
||||
setAvatar,
|
||||
name = "",
|
||||
nameDisabled = false,
|
||||
setName,
|
||||
topic = "",
|
||||
topicDisabled = false,
|
||||
setTopic,
|
||||
}: IProps) => {
|
||||
const avatarUploadRef = useRef<HTMLInputElement>();
|
||||
const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
|
||||
|
||||
let avatarSection;
|
||||
if (avatarDisabled) {
|
||||
if (avatar) {
|
||||
avatarSection = <img className="mx_SpaceBasicSettings_avatar" src={avatar} alt="" />;
|
||||
} else {
|
||||
avatarSection = <div className="mx_SpaceBasicSettings_avatar" />;
|
||||
}
|
||||
} else {
|
||||
if (avatar) {
|
||||
avatarSection = <React.Fragment>
|
||||
<AccessibleButton
|
||||
className="mx_SpaceBasicSettings_avatar"
|
||||
onClick={() => avatarUploadRef.current?.click()}
|
||||
element="img"
|
||||
src={avatar}
|
||||
alt=""
|
||||
/>
|
||||
<AccessibleButton onClick={() => {
|
||||
avatarUploadRef.current.value = "";
|
||||
setAvatarDataUrl(undefined);
|
||||
setAvatar(undefined);
|
||||
}} kind="link" className="mx_SpaceBasicSettings_avatar_remove">
|
||||
{ _t("Delete") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
avatarSection = <React.Fragment>
|
||||
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
|
||||
<AccessibleButton onClick={() => avatarUploadRef.current?.click()} kind="link">
|
||||
{ _t("Upload") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceBasicSettings">
|
||||
<div className="mx_SpaceBasicSettings_avatarContainer">
|
||||
{ avatarSection }
|
||||
<input type="file" ref={avatarUploadRef} onChange={(e) => {
|
||||
if (!e.target.files?.length) return;
|
||||
const file = e.target.files[0];
|
||||
setAvatar(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
setAvatarDataUrl(ev.target.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}} accept="image/*" />
|
||||
</div>
|
||||
|
||||
<Field
|
||||
name="spaceName"
|
||||
label={_t("Name")}
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={ev => setName(ev.target.value)}
|
||||
disabled={nameDisabled}
|
||||
/>
|
||||
|
||||
<Field
|
||||
name="spaceTopic"
|
||||
element="textarea"
|
||||
label={_t("Description")}
|
||||
value={topic}
|
||||
onChange={ev => setTopic(ev.target.value)}
|
||||
rows={3}
|
||||
disabled={topicDisabled}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpaceBasicSettings;
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import createRoom, {IStateEvent, Preset} from "../../../createRoom";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import SpaceBasicSettings from "./SpaceBasicSettings";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import FocusLock from "react-focus-lock";
|
||||
|
||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||
return (
|
||||
<AccessibleButton className={classNames("mx_SpaceCreateMenuType", className)} onClick={onClick}>
|
||||
<h3>{ title }</h3>
|
||||
<span>{ description }</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
enum Visibility {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
const SpaceCreateMenu = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [avatar, setAvatar] = useState<File>(null);
|
||||
const [topic, setTopic] = useState<string>("");
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
const onSpaceCreateClick = async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
const initialState: IStateEvent[] = [
|
||||
{
|
||||
type: EventType.RoomHistoryVisibility,
|
||||
content: {
|
||||
"history_visibility": visibility === Visibility.Public ? "world_readable" : "invited",
|
||||
},
|
||||
},
|
||||
];
|
||||
if (avatar) {
|
||||
const url = await cli.uploadContent(avatar);
|
||||
|
||||
initialState.push({
|
||||
type: EventType.RoomAvatar,
|
||||
content: { url },
|
||||
});
|
||||
}
|
||||
if (topic) {
|
||||
initialState.push({
|
||||
type: EventType.RoomTopic,
|
||||
content: { topic },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await createRoom({
|
||||
createOpts: {
|
||||
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
creation_content: {
|
||||
// Based on MSC1840
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
initial_state: initialState,
|
||||
power_level_content_override: {
|
||||
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
||||
events_default: 100,
|
||||
},
|
||||
},
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: true,
|
||||
inlineErrors: true,
|
||||
});
|
||||
|
||||
onFinished();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
let body;
|
||||
if (visibility === null) {
|
||||
body = <React.Fragment>
|
||||
<h2>{ _t("Create a space") }</h2>
|
||||
<p>{ _t("Organise rooms into spaces, for just you or anyone") }</p>
|
||||
|
||||
<SpaceCreateMenuType
|
||||
title={_t("Public")}
|
||||
description={_t("Open space for anyone, best for communities")}
|
||||
className="mx_SpaceCreateMenuType_public"
|
||||
onClick={() => setVisibility(Visibility.Public)}
|
||||
/>
|
||||
<SpaceCreateMenuType
|
||||
title={_t("Private")}
|
||||
description={_t("Invite only space, best for yourself or teams")}
|
||||
className="mx_SpaceCreateMenuType_private"
|
||||
onClick={() => setVisibility(Visibility.Private)}
|
||||
/>
|
||||
|
||||
{/*<p>{ _t("Looking to join an existing space?") }</p>*/}
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
body = <React.Fragment>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_SpaceCreateMenu_back"
|
||||
onClick={() => setVisibility(null)}
|
||||
title={_t("Go back")}
|
||||
/>
|
||||
|
||||
<h2>
|
||||
{
|
||||
visibility === Visibility.Public
|
||||
? _t("Personalise your public space")
|
||||
: _t("Personalise your private space")
|
||||
}
|
||||
</h2>
|
||||
<p>
|
||||
{
|
||||
_t("Give it a photo, name and description to help you identify it.")
|
||||
} {
|
||||
_t("You can change these at any point.")
|
||||
}
|
||||
</p>
|
||||
|
||||
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
|
||||
|
||||
<FormButton
|
||||
label={busy ? _t("Creating...") : _t("Create")}
|
||||
onClick={onSpaceCreateClick}
|
||||
disabled={!name && !busy}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <ContextMenu
|
||||
left={72}
|
||||
top={62}
|
||||
chevronOffset={0}
|
||||
chevronFace={ChevronFace.None}
|
||||
onFinished={onFinished}
|
||||
wrapperClassName="mx_SpaceCreateMenu_wrapper"
|
||||
managed={false}
|
||||
>
|
||||
<FocusLock returnFocus={true}>
|
||||
{ body }
|
||||
</FocusLock>
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
||||
export default SpaceCreateMenu;
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {useContextMenu} from "../../structures/ContextMenu";
|
||||
import SpaceCreateMenu from "./SpaceCreateMenu";
|
||||
import {SpaceItem} from "./SpaceTreeLevel";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {
|
||||
RovingAccessibleButton,
|
||||
RovingAccessibleTooltipButton,
|
||||
RovingTabIndexProvider,
|
||||
} from "../../../accessibility/RovingTabIndex";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
interface IButtonProps {
|
||||
space?: Room;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
tooltip?: string;
|
||||
notificationState?: SpaceNotificationState;
|
||||
isNarrow?: boolean;
|
||||
onClick(): void;
|
||||
}
|
||||
|
||||
const SpaceButton: React.FC<IButtonProps> = ({
|
||||
space,
|
||||
className,
|
||||
selected,
|
||||
onClick,
|
||||
tooltip,
|
||||
notificationState,
|
||||
isNarrow,
|
||||
children,
|
||||
}) => {
|
||||
const classes = classNames("mx_SpaceButton", className, {
|
||||
mx_SpaceButton_active: selected,
|
||||
mx_SpaceButton_narrow: isNarrow,
|
||||
});
|
||||
|
||||
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
|
||||
if (space) {
|
||||
avatar = <RoomAvatar width={32} height={32} room={space} />;
|
||||
}
|
||||
|
||||
let notifBadge;
|
||||
if (notificationState) {
|
||||
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||
<NotificationBadge forceCount={false} notification={notificationState} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let button;
|
||||
if (isNarrow) {
|
||||
button = (
|
||||
<RovingAccessibleTooltipButton className={classes} title={tooltip} onClick={onClick} role="treeitem">
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
{ avatar }
|
||||
{ notifBadge }
|
||||
{ children }
|
||||
</div>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<RovingAccessibleButton className={classes} onClick={onClick} role="treeitem">
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
{ avatar }
|
||||
<span className="mx_SpaceButton_name">{ tooltip }</span>
|
||||
{ notifBadge }
|
||||
{ children }
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <li className={classNames({
|
||||
"mx_SpaceItem": true,
|
||||
"collapsed": isNarrow,
|
||||
})}>
|
||||
{ button }
|
||||
</li>;
|
||||
}
|
||||
|
||||
const useSpaces = (): [Room[], Room | null] => {
|
||||
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
||||
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
||||
return [spaces, activeSpace];
|
||||
};
|
||||
|
||||
const SpacePanel = () => {
|
||||
// We don't need the handle as we position the menu in a constant location
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||
const [spaces, activeSpace] = useSpaces();
|
||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||
|
||||
const newClasses = classNames("mx_SpaceButton_new", {
|
||||
mx_SpaceButton_newCancel: menuDisplayed,
|
||||
});
|
||||
|
||||
let contextMenu = null;
|
||||
if (menuDisplayed) {
|
||||
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
|
||||
}
|
||||
|
||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_UP:
|
||||
onMoveFocus(ev.target as Element, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
onMoveFocus(ev.target as Element, false);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
// consume all other keys in context menu
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onMoveFocus = (element: Element, up: boolean) => {
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes: DOMTokenList;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && !classes.contains("mx_SpaceButton"));
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
|
||||
// TODO drag and drop for re-arranging order
|
||||
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({onKeyDownHandler}) => (
|
||||
<ul
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
>
|
||||
<AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
|
||||
<div className="mx_SpaceTreeLevel">
|
||||
<SpaceButton
|
||||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
selected={!activeSpace}
|
||||
tooltip={_t("Home")}
|
||||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
{ spaces.map(s => <SpaceItem
|
||||
key={s.roomId}
|
||||
space={s}
|
||||
activeSpaces={activeSpaces}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
onExpand={() => setPanelCollapsed(false)}
|
||||
/>) }
|
||||
</div>
|
||||
<SpaceButton
|
||||
className={newClasses}
|
||||
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||
onClick={menuDisplayed ? closeMenu : openMenu}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
</AutoHideScrollbar>
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
||||
onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
|
||||
title={expandCollapseButtonTitle}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</ul>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
};
|
||||
|
||||
export default SpacePanel;
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import {showSpaceInviteDialog} from "../../../RoomInvite";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
||||
const [copiedText, setCopiedText] = useState(_t("Click to copy"));
|
||||
|
||||
return <div className="mx_SpacePublicShare">
|
||||
<AccessibleButton
|
||||
className="mx_SpacePublicShare_shareButton"
|
||||
onClick={async () => {
|
||||
const permalinkCreator = new RoomPermalinkCreator(space);
|
||||
permalinkCreator.load();
|
||||
const success = await copyPlaintext(permalinkCreator.forRoom());
|
||||
const text = success ? _t("Copied!") : _t("Failed to copy");
|
||||
setCopiedText(text);
|
||||
await sleep(10);
|
||||
if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
|
||||
setCopiedText(_t("Click to copy"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ _t("Share invite link") }
|
||||
<span>{ copiedText }</span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_SpacePublicShare_inviteButton"
|
||||
onClick={() => {
|
||||
showSpaceInviteDialog(space.roomId);
|
||||
onFinished();
|
||||
}}
|
||||
>
|
||||
{ _t("Invite by email or username") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpacePublicShare;
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
|
||||
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IItemProps {
|
||||
space?: Room;
|
||||
activeSpaces: Room[];
|
||||
isNested?: boolean;
|
||||
isPanelCollapsed?: boolean;
|
||||
onExpand?: Function;
|
||||
}
|
||||
|
||||
interface IItemState {
|
||||
collapsed: boolean;
|
||||
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
|
||||
}
|
||||
|
||||
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
collapsed: !props.isNested, // default to collapsed for root items
|
||||
contextMenuPosition: null,
|
||||
};
|
||||
}
|
||||
|
||||
private toggleCollapse(evt) {
|
||||
if (this.props.onExpand && this.state.collapsed) {
|
||||
this.props.onExpand();
|
||||
}
|
||||
this.setState({collapsed: !this.state.collapsed});
|
||||
// don't bubble up so encapsulating button for space
|
||||
// doesn't get triggered
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
private onContextMenu = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
contextMenuPosition: {
|
||||
right: ev.clientX,
|
||||
top: ev.clientY,
|
||||
height: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
SpaceStore.instance.setActiveSpace(this.props.space);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {space, activeSpaces, isNested} = this.props;
|
||||
|
||||
const forceCollapsed = this.props.isPanelCollapsed;
|
||||
const isNarrow = this.props.isPanelCollapsed;
|
||||
const collapsed = this.state.collapsed || forceCollapsed;
|
||||
|
||||
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const isActive = activeSpaces.includes(space);
|
||||
const itemClasses = classNames({
|
||||
"mx_SpaceItem": true,
|
||||
"collapsed": collapsed,
|
||||
"hasSubSpaces": childSpaces && childSpaces.length,
|
||||
});
|
||||
const classes = classNames("mx_SpaceButton", {
|
||||
mx_SpaceButton_active: isActive,
|
||||
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||
mx_SpaceButton_narrow: isNarrow,
|
||||
});
|
||||
const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
|
||||
const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
|
||||
spaces={childSpaces}
|
||||
activeSpaces={activeSpaces}
|
||||
isNested={true}
|
||||
/> : null;
|
||||
let notifBadge;
|
||||
if (notificationState) {
|
||||
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||
<NotificationBadge forceCount={false} notification={notificationState} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarSize = isNested ? 24 : 32;
|
||||
|
||||
const toggleCollapseButton = childSpaces && childSpaces.length ?
|
||||
<button
|
||||
className="mx_SpaceButton_toggleCollapse"
|
||||
onClick={evt => this.toggleCollapse(evt)}
|
||||
/> : null;
|
||||
|
||||
let button;
|
||||
if (isNarrow) {
|
||||
button = (
|
||||
<RovingAccessibleTooltipButton
|
||||
className={classes}
|
||||
title={space.name}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
forceHide={!!this.state.contextMenuPosition}
|
||||
role="treeitem"
|
||||
>
|
||||
{ toggleCollapseButton }
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
{ notifBadge }
|
||||
</div>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<RovingAccessibleButton
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
role="treeitem"
|
||||
>
|
||||
{ toggleCollapseButton }
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
<span className="mx_SpaceButton_name">{ space.name }</span>
|
||||
{ notifBadge }
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={itemClasses}>
|
||||
{ button }
|
||||
{ childItems }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ITreeLevelProps {
|
||||
spaces: Room[];
|
||||
activeSpaces: Room[];
|
||||
isNested?: boolean;
|
||||
}
|
||||
|
||||
const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
|
||||
spaces,
|
||||
activeSpaces,
|
||||
isNested,
|
||||
}) => {
|
||||
return <ul className="mx_SpaceTreeLevel">
|
||||
{spaces.map(s => {
|
||||
return (<SpaceItem
|
||||
key={s.roomId}
|
||||
activeSpaces={activeSpaces}
|
||||
space={s}
|
||||
isNested={isNested}
|
||||
/>);
|
||||
})}
|
||||
</ul>;
|
||||
}
|
||||
|
||||
export default SpaceTreeLevel;
|
|
@ -518,7 +518,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : (
|
||||
const maxVideoHeight = getFullScreenElement() || !this.props.maxVideoHeight ? null : (
|
||||
this.props.maxVideoHeight - (HEADER_HEIGHT + BOTTOM_PADDING + BOTTOM_MARGIN_TOP_BOTTOM)
|
||||
);
|
||||
contentView = <div className={containerClasses}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import Modal from './Modal';
|
||||
|
@ -31,6 +32,8 @@ import GroupStore from "./stores/GroupStore";
|
|||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import { isJoinedOrNearlyJoined } from "./utils/membership";
|
||||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
|
||||
import SpaceStore from "./stores/SpaceStore";
|
||||
import { makeSpaceParentEvent } from "./utils/space";
|
||||
|
||||
// we define a number of interfaces which take their names from the js-sdk
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -41,7 +44,7 @@ enum Visibility {
|
|||
Private = "private",
|
||||
}
|
||||
|
||||
enum Preset {
|
||||
export enum Preset {
|
||||
PrivateChat = "private_chat",
|
||||
TrustedPrivateChat = "trusted_private_chat",
|
||||
PublicChat = "public_chat",
|
||||
|
@ -54,7 +57,7 @@ interface Invite3PID {
|
|||
address: string;
|
||||
}
|
||||
|
||||
interface IStateEvent {
|
||||
export interface IStateEvent {
|
||||
type: string;
|
||||
state_key?: string; // defaults to an empty string
|
||||
content: object;
|
||||
|
@ -75,7 +78,7 @@ interface ICreateOpts {
|
|||
power_level_content_override?: object;
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
export interface IOpts {
|
||||
dmUserId?: string;
|
||||
createOpts?: ICreateOpts;
|
||||
spinner?: boolean;
|
||||
|
@ -84,6 +87,7 @@ interface IOpts {
|
|||
inlineErrors?: boolean;
|
||||
andView?: boolean;
|
||||
associatedWithCommunity?: string;
|
||||
parentSpace?: Room;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -175,6 +179,16 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
|||
});
|
||||
}
|
||||
|
||||
if (opts.parentSpace) {
|
||||
opts.createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
|
||||
opts.createOpts.initial_state.push({
|
||||
type: EventType.RoomHistoryVisibility,
|
||||
content: {
|
||||
"history_visibility": opts.createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let modal;
|
||||
if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
|
@ -189,6 +203,9 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
|||
return Promise.resolve();
|
||||
}
|
||||
}).then(() => {
|
||||
if (opts.parentSpace) {
|
||||
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], true);
|
||||
}
|
||||
if (opts.associatedWithCommunity) {
|
||||
return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false);
|
||||
}
|
||||
|
@ -197,6 +214,9 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
|||
// room has been created, so we race here with the client knowing that
|
||||
// the room exists, causing things like
|
||||
// https://github.com/vector-im/vector-web/issues/1813
|
||||
// Even if we were to block on the echo, servers tend to split the room
|
||||
// state over multiple syncs so we can't atomically know when we have the
|
||||
// entire thing.
|
||||
if (opts.andView) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
|
@ -206,6 +226,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
|||
// so we are expecting the room to come down the sync
|
||||
// stream, if it hasn't already.
|
||||
joining: true,
|
||||
justCreatedOpts: opts,
|
||||
});
|
||||
}
|
||||
CountlyAnalytics.instance.trackRoomCreate(startTime, roomId);
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import { ActionPayload } from "../payloads";
|
||||
|
@ -35,4 +36,5 @@ export interface SetRightPanelPhaseRefireParams {
|
|||
// XXX: The type for event should 'view_3pid_invite' action's payload
|
||||
event?: any;
|
||||
widgetId?: string;
|
||||
space?: Room;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,11 @@ function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
|
|||
}
|
||||
}
|
||||
|
||||
function parseImage(img: HTMLImageElement, partCreator: PartCreator) {
|
||||
const { src } = img;
|
||||
return partCreator.plain(`![${img.alt.replace(/[[\\\]]/g, c => "\\" + c)}](${src})`);
|
||||
}
|
||||
|
||||
function parseCodeBlock(n: HTMLElement, partCreator: PartCreator) {
|
||||
const parts = [];
|
||||
let language = "";
|
||||
|
@ -102,6 +107,8 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
|
|||
return parseHeader(n, partCreator);
|
||||
case "A":
|
||||
return parseLink(<HTMLAnchorElement>n, partCreator);
|
||||
case "IMG":
|
||||
return parseImage(<HTMLImageElement>n, partCreator);
|
||||
case "BR":
|
||||
return partCreator.newline();
|
||||
case "EM":
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {useState} from "react";
|
||||
|
||||
// Hook to simplify managing state of arrays of a common type
|
||||
export const useStateArray = <T>(initialSize: number, initialState: T | T[]): [T[], (i: number, v: T) => void] => {
|
||||
const [data, setData] = useState<T[]>(() => {
|
||||
return Array.isArray(initialState) ? initialState : new Array(initialSize).fill(initialState);
|
||||
});
|
||||
return [data, (index: number, value: T) => setData(data => {
|
||||
const copy = [...data];
|
||||
copy[index] = value;
|
||||
return copy;
|
||||
})]
|
||||
};
|
|
@ -978,8 +978,32 @@
|
|||
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
|
||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||
"Remove": "Remove",
|
||||
"Delete": "Delete",
|
||||
"Upload": "Upload",
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
"Create a space": "Create a space",
|
||||
"Organise rooms into spaces, for just you or anyone": "Organise rooms into spaces, for just you or anyone",
|
||||
"Public": "Public",
|
||||
"Open space for anyone, best for communities": "Open space for anyone, best for communities",
|
||||
"Private": "Private",
|
||||
"Invite only space, best for yourself or teams": "Invite only space, best for yourself or teams",
|
||||
"Go back": "Go back",
|
||||
"Personalise your public space": "Personalise your public space",
|
||||
"Personalise your private space": "Personalise your private space",
|
||||
"Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.",
|
||||
"You can change these at any point.": "You can change these at any point.",
|
||||
"Creating...": "Creating...",
|
||||
"Create": "Create",
|
||||
"Expand space panel": "Expand space panel",
|
||||
"Collapse space panel": "Collapse space panel",
|
||||
"Home": "Home",
|
||||
"Click to copy": "Click to copy",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
"Share invite link": "Share invite link",
|
||||
"Invite by email or username": "Invite by email or username",
|
||||
"Remove": "Remove",
|
||||
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
|
||||
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
|
||||
"Workspace: <networkLink/>": "Workspace: <networkLink/>",
|
||||
|
@ -1133,7 +1157,6 @@
|
|||
"Disconnect anyway": "Disconnect anyway",
|
||||
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "You are still <b>sharing your personal data</b> on the identity server <idserver />.",
|
||||
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.",
|
||||
"Go back": "Go back",
|
||||
"Identity Server (%(server)s)": "Identity Server (%(server)s)",
|
||||
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.",
|
||||
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.",
|
||||
|
@ -1148,6 +1171,7 @@
|
|||
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
|
||||
"Manage integrations": "Manage integrations",
|
||||
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
|
||||
"Add": "Add",
|
||||
"Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
|
||||
"Checking for an update...": "Checking for an update...",
|
||||
"No update available.": "No update available.",
|
||||
|
@ -1179,6 +1203,7 @@
|
|||
"Set a new account password...": "Set a new account password...",
|
||||
"Account": "Account",
|
||||
"Language and region": "Language and region",
|
||||
"Spell check dictionaries": "Spell check dictionaries",
|
||||
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
||||
"Account management": "Account management",
|
||||
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
|
||||
|
@ -1376,7 +1401,6 @@
|
|||
"Invalid Email Address": "Invalid Email Address",
|
||||
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
|
||||
"Unable to add email address": "Unable to add email address",
|
||||
"Add": "Add",
|
||||
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
|
||||
"Email Address": "Email Address",
|
||||
"Remove %(phone)s?": "Remove %(phone)s?",
|
||||
|
@ -1796,8 +1820,6 @@
|
|||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
||||
"Click here to see older messages.": "Click here to see older messages.",
|
||||
"This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
"Add an Integration": "Add an Integration",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
|
||||
"Edited at %(date)s": "Edited at %(date)s",
|
||||
|
@ -1941,7 +1963,6 @@
|
|||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
"And %(count)s more...|other": "And %(count)s more...",
|
||||
"Home": "Home",
|
||||
"Enter a server name": "Enter a server name",
|
||||
"Looks good": "Looks good",
|
||||
"Can't find this server or its room list": "Can't find this server or its room list",
|
||||
|
@ -2009,7 +2030,6 @@
|
|||
"You can change this later if needed.": "You can change this later if needed.",
|
||||
"What's the name of your community or team?": "What's the name of your community or team?",
|
||||
"Enter name": "Enter name",
|
||||
"Create": "Create",
|
||||
"Add image (optional)": "Add image (optional)",
|
||||
"An image will help people identify your community.": "An image will help people identify your community.",
|
||||
"Community IDs cannot be empty.": "Community IDs cannot be empty.",
|
||||
|
@ -2031,7 +2051,6 @@
|
|||
"Create a public room": "Create a public room",
|
||||
"Create a private room": "Create a private room",
|
||||
"Create a room in %(communityName)s": "Create a room in %(communityName)s",
|
||||
"Name": "Name",
|
||||
"Topic (optional)": "Topic (optional)",
|
||||
"Make this room public": "Make this room public",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
||||
|
@ -2149,8 +2168,11 @@
|
|||
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
|
||||
"Go": "Go",
|
||||
"Invite to this space": "Invite to this space",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
|
||||
"Transfer": "Transfer",
|
||||
"a new master key signature": "a new master key signature",
|
||||
"a new cross-signing key signature": "a new cross-signing key signature",
|
||||
|
@ -2454,7 +2476,6 @@
|
|||
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
|
||||
"Long Description (HTML)": "Long Description (HTML)",
|
||||
"Upload avatar": "Upload avatar",
|
||||
"Description": "Description",
|
||||
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
||||
"This homeserver does not support communities": "This homeserver does not support communities",
|
||||
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
||||
|
@ -2536,6 +2557,37 @@
|
|||
"Failed to reject invite": "Failed to reject invite",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
|
||||
"Accept Invite": "Accept Invite",
|
||||
"%(count)s members|other": "%(count)s members",
|
||||
"%(count)s members|one": "%(count)s member",
|
||||
"<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
|
||||
"You have been invited to <name/>": "You have been invited to <name/>",
|
||||
"Your public space <name/>": "Your public space <name/>",
|
||||
"Your private space <name/>": "Your private space <name/>",
|
||||
"Welcome to <name/>": "Welcome to <name/>",
|
||||
"Random": "Random",
|
||||
"Support": "Support",
|
||||
"Room name": "Room name",
|
||||
"Failed to create initial space rooms": "Failed to create initial space rooms",
|
||||
"Skip for now": "Skip for now",
|
||||
"Creating rooms...": "Creating rooms...",
|
||||
"Share your public space": "Share your public space",
|
||||
"At the moment only you can see it.": "At the moment only you can see it.",
|
||||
"Finish": "Finish",
|
||||
"Who are you working with?": "Who are you working with?",
|
||||
"Ensure the right people have access to the space.": "Ensure the right people have access to the space.",
|
||||
"Just Me": "Just Me",
|
||||
"A private space just for you": "A private space just for you",
|
||||
"Me and my teammates": "Me and my teammates",
|
||||
"A private space for you and your teammates": "A private space for you and your teammates",
|
||||
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
|
||||
"Invite your teammates": "Invite your teammates",
|
||||
"Invite by username": "Invite by username",
|
||||
"Inviting...": "Inviting...",
|
||||
"What discussions do you want to have?": "What discussions do you want to have?",
|
||||
"We'll create rooms for each topic.": "We'll create rooms for each topic.",
|
||||
"What projects are you working on?": "What projects are you working on?",
|
||||
"We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.",
|
||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
"Failed to load timeline position": "Failed to load timeline position",
|
||||
|
|
|
@ -31,6 +31,11 @@ export enum RightPanelPhases {
|
|||
GroupRoomList = 'GroupRoomList',
|
||||
GroupRoomInfo = 'GroupRoomInfo',
|
||||
GroupMemberInfo = 'GroupMemberInfo',
|
||||
|
||||
// Space stuff
|
||||
SpaceMemberList = "SpaceMemberList",
|
||||
SpaceMemberInfo = "SpaceMemberInfo",
|
||||
Space3pidMemberInfo = "Space3pidMemberInfo",
|
||||
}
|
||||
|
||||
// These are the phases that are safe to persist (the ones that don't require additional
|
||||
|
@ -43,3 +48,10 @@ export const RIGHT_PANEL_PHASES_NO_ARGS = [
|
|||
RightPanelPhases.GroupMemberList,
|
||||
RightPanelPhases.GroupRoomList,
|
||||
];
|
||||
|
||||
// Subset of phases visible in the Space View
|
||||
export const RIGHT_PANEL_SPACE_PHASES = [
|
||||
RightPanelPhases.SpaceMemberList,
|
||||
RightPanelPhases.Space3pidMemberInfo,
|
||||
RightPanelPhases.SpaceMemberInfo,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,462 @@
|
|||
/*
|
||||
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 {throttle, sortBy} from "lodash";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {AsyncStoreWithClient} from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import {ActionPayload} from "../dispatcher/payloads";
|
||||
import RoomListStore from "./room-list/RoomListStore";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import DMRoomMap from "../utils/DMRoomMap";
|
||||
import {FetchRoomFn} from "./notifications/ListNotificationState";
|
||||
import {SpaceNotificationState} from "./notifications/SpaceNotificationState";
|
||||
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
|
||||
import {DefaultTagID} from "./room-list/models";
|
||||
import {EnhancedMap, mapDiff} from "../utils/maps";
|
||||
import {setHasDiff} from "../utils/sets";
|
||||
import {objectDiff} from "../utils/objects";
|
||||
import {arrayHasDiff} from "../utils/arrays";
|
||||
|
||||
type SpaceKey = string | symbol;
|
||||
|
||||
interface IState {}
|
||||
|
||||
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
||||
|
||||
export const HOME_SPACE = Symbol("home-space");
|
||||
|
||||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
||||
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
||||
|
||||
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
||||
return arr.reduce((result, room: Room) => {
|
||||
result[room.isSpaceRoom() ? 0 : 1].push(room);
|
||||
return result;
|
||||
}, [[], []]);
|
||||
};
|
||||
|
||||
const getOrder = (ev: MatrixEvent): string | null => {
|
||||
const content = ev.getContent();
|
||||
if (typeof content.order === "string" && Array.from(content.order).every((c: string) => {
|
||||
const charCode = c.charCodeAt(0);
|
||||
return charCode >= 0x20 && charCode <= 0x7F;
|
||||
})) {
|
||||
return content.order;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||
return RoomNotificationStateStore.instance.getRoomState(room);
|
||||
};
|
||||
|
||||
export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
// The spaces representing the roots of the various tree-like hierarchies
|
||||
private rootSpaces: Room[] = [];
|
||||
// The list of rooms not present in any currently joined spaces
|
||||
private orphanedRooms = new Set<string>();
|
||||
// Map from room ID to set of spaces which list it as a child
|
||||
private parentMap = new EnhancedMap<string, Set<string>>();
|
||||
// Map from space key to SpaceNotificationState instance representing that space
|
||||
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
||||
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
||||
private spaceFilteredRooms = new Map<string | symbol, Set<string>>();
|
||||
// The space currently selected in the Space Panel - if null then `Home` is selected
|
||||
private _activeSpace?: Room = null;
|
||||
|
||||
public get spacePanelSpaces(): Room[] {
|
||||
return this.rootSpaces;
|
||||
}
|
||||
|
||||
public get activeSpace(): Room | null {
|
||||
return this._activeSpace || null;
|
||||
}
|
||||
|
||||
public setActiveSpace(space: Room | null) {
|
||||
if (space === this.activeSpace) return;
|
||||
|
||||
this._activeSpace = space;
|
||||
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
|
||||
|
||||
// persist space selected
|
||||
if (space) {
|
||||
window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId);
|
||||
} else {
|
||||
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
public addRoomToSpace(space: Room, roomId: string, via: string[], autoJoin = false) {
|
||||
return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, {
|
||||
via,
|
||||
auto_join: autoJoin,
|
||||
}, roomId);
|
||||
}
|
||||
|
||||
private getChildren(spaceId: string): Room[] {
|
||||
const room = this.matrixClient?.getRoom(spaceId);
|
||||
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
|
||||
return sortBy(childEvents, getOrder)
|
||||
.map(ev => this.matrixClient.getRoom(ev.getStateKey()))
|
||||
.filter(room => room?.getMyMembership() === "join") || [];
|
||||
}
|
||||
|
||||
public getChildRooms(spaceId: string): Room[] {
|
||||
return this.getChildren(spaceId).filter(r => !r.isSpaceRoom());
|
||||
}
|
||||
|
||||
public getChildSpaces(spaceId: string): Room[] {
|
||||
return this.getChildren(spaceId).filter(r => r.isSpaceRoom());
|
||||
}
|
||||
|
||||
public getParents(roomId: string, canonicalOnly = false): Room[] {
|
||||
const room = this.matrixClient?.getRoom(roomId);
|
||||
return room?.currentState.getStateEvents(EventType.SpaceParent)
|
||||
.filter(ev => {
|
||||
const content = ev.getContent();
|
||||
if (!content?.via) return false;
|
||||
// TODO apply permissions check to verify that the parent mapping is valid
|
||||
if (canonicalOnly && !content?.canonical) return false;
|
||||
return true;
|
||||
})
|
||||
.map(ev => this.matrixClient.getRoom(ev.getStateKey()))
|
||||
.filter(Boolean) || [];
|
||||
}
|
||||
|
||||
public getCanonicalParent(roomId: string): Room | null {
|
||||
const parents = this.getParents(roomId, true);
|
||||
return sortBy(parents, r => r.roomId)?.[0] || null;
|
||||
}
|
||||
|
||||
public getSpaces = () => {
|
||||
return this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "join");
|
||||
};
|
||||
|
||||
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
||||
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
|
||||
};
|
||||
|
||||
public rebuild = throttle(() => { // exported for tests
|
||||
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||
|
||||
// Sort spaces by room ID to force the loop breaking to be deterministic
|
||||
const spaces = sortBy(this.getSpaces(), space => space.roomId);
|
||||
const unseenChildren = new Set<Room>([...visibleRooms, ...spaces]);
|
||||
|
||||
const backrefs = new EnhancedMap<string, Set<string>>();
|
||||
|
||||
// TODO handle cleaning up links when a Space is removed
|
||||
spaces.forEach(space => {
|
||||
const children = this.getChildren(space.roomId);
|
||||
children.forEach(child => {
|
||||
unseenChildren.delete(child);
|
||||
|
||||
backrefs.getOrCreate(child.roomId, new Set()).add(space.roomId);
|
||||
});
|
||||
});
|
||||
|
||||
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
||||
|
||||
// untested algorithm to handle full-cycles
|
||||
const detachedNodes = new Set<Room>(spaces);
|
||||
|
||||
const markTreeChildren = (rootSpace: Room, unseen: Set<Room>) => {
|
||||
const stack = [rootSpace];
|
||||
while (stack.length) {
|
||||
const op = stack.pop();
|
||||
unseen.delete(op);
|
||||
this.getChildSpaces(op.roomId).forEach(space => {
|
||||
if (unseen.has(space)) {
|
||||
stack.push(space);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
rootSpaces.forEach(rootSpace => {
|
||||
markTreeChildren(rootSpace, detachedNodes);
|
||||
});
|
||||
|
||||
// Handle spaces forming fully cyclical relationships.
|
||||
// In order, assume each detachedNode is a root unless it has already
|
||||
// been claimed as the child of prior detached node.
|
||||
// Work from a copy of the detachedNodes set as it will be mutated as part of this operation.
|
||||
Array.from(detachedNodes).forEach(detachedNode => {
|
||||
if (!detachedNodes.has(detachedNode)) return;
|
||||
// declare this detached node a new root, find its children, without ever looping back to it
|
||||
detachedNodes.delete(detachedNode);
|
||||
rootSpaces.push(detachedNode);
|
||||
markTreeChildren(detachedNode, detachedNodes);
|
||||
|
||||
// TODO only consider a detached node a root space if it has no *parents other than the ones forming cycles
|
||||
});
|
||||
|
||||
// TODO neither of these handle an A->B->C->A with an additional C->D
|
||||
// detachedNodes.forEach(space => {
|
||||
// rootSpaces.push(space);
|
||||
// });
|
||||
|
||||
this.orphanedRooms = new Set(orphanedRooms);
|
||||
this.rootSpaces = rootSpaces;
|
||||
this.parentMap = backrefs;
|
||||
|
||||
// if the currently selected space no longer exists, remove its selection
|
||||
if (this._activeSpace && detachedNodes.has(this._activeSpace)) {
|
||||
this.setActiveSpace(null);
|
||||
}
|
||||
|
||||
this.onRoomsUpdate(); // TODO only do this if a change has happened
|
||||
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
|
||||
}, 100, {trailing: true, leading: true});
|
||||
|
||||
onSpaceUpdate = () => {
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
private showInHomeSpace = (room: Room) => {
|
||||
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
||||
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|
||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
|
||||
};
|
||||
|
||||
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
|
||||
// This can only change whether it shows up in the HOME_SPACE or not
|
||||
private onRoomUpdate = (room: Room) => {
|
||||
if (this.showInHomeSpace(room)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
|
||||
this.emit(HOME_SPACE);
|
||||
} else if (!this.orphanedRooms.has(room.roomId)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
|
||||
this.emit(HOME_SPACE);
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomsUpdate = throttle(() => {
|
||||
// TODO resolve some updates as deltas
|
||||
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||
|
||||
const oldFilteredRooms = this.spaceFilteredRooms;
|
||||
this.spaceFilteredRooms = new Map();
|
||||
|
||||
// put all invites (rooms & spaces) in the Home Space
|
||||
const invites = this.matrixClient.getRooms().filter(r => r.getMyMembership() === "invite");
|
||||
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
|
||||
|
||||
visibleRooms.forEach(room => {
|
||||
if (this.showInHomeSpace(room)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
|
||||
}
|
||||
});
|
||||
|
||||
this.rootSpaces.forEach(s => {
|
||||
// traverse each space tree in DFS to build up the supersets as you go up,
|
||||
// reusing results from like subtrees.
|
||||
const fn = (spaceId: string, parentPath: Set<string>): Set<string> => {
|
||||
if (parentPath.has(spaceId)) return; // prevent cycles
|
||||
|
||||
// reuse existing results if multiple similar branches exist
|
||||
if (this.spaceFilteredRooms.has(spaceId)) {
|
||||
return this.spaceFilteredRooms.get(spaceId);
|
||||
}
|
||||
|
||||
const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
|
||||
const roomIds = new Set(childRooms.map(r => r.roomId));
|
||||
const space = this.matrixClient?.getRoom(spaceId);
|
||||
|
||||
// Add relevant DMs
|
||||
space?.getJoinedMembers().forEach(member => {
|
||||
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
|
||||
roomIds.add(roomId);
|
||||
});
|
||||
});
|
||||
|
||||
const newPath = new Set(parentPath).add(spaceId);
|
||||
childSpaces.forEach(childSpace => {
|
||||
fn(childSpace.roomId, newPath)?.forEach(roomId => {
|
||||
roomIds.add(roomId);
|
||||
});
|
||||
});
|
||||
this.spaceFilteredRooms.set(spaceId, roomIds);
|
||||
return roomIds;
|
||||
};
|
||||
|
||||
fn(s.roomId, new Set());
|
||||
});
|
||||
|
||||
const diff = mapDiff(oldFilteredRooms, this.spaceFilteredRooms);
|
||||
// filter out keys which changed by reference only by checking whether the sets differ
|
||||
const changed = diff.changed.filter(k => setHasDiff(oldFilteredRooms.get(k), this.spaceFilteredRooms.get(k)));
|
||||
[...diff.added, ...diff.removed, ...changed].forEach(k => {
|
||||
this.emit(k);
|
||||
});
|
||||
|
||||
this.spaceFilteredRooms.forEach((roomIds, s) => {
|
||||
// Update NotificationStates
|
||||
const rooms = this.matrixClient.getRooms().filter(room => roomIds.has(room.roomId));
|
||||
this.getNotificationState(s)?.setRooms(rooms);
|
||||
});
|
||||
}, 100, {trailing: true, leading: true});
|
||||
|
||||
private onRoom = (room: Room) => {
|
||||
if (room?.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
this.emit(room.roomId);
|
||||
} else {
|
||||
// this.onRoomUpdate(room);
|
||||
this.onRoomsUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomState = (ev: MatrixEvent) => {
|
||||
const room = this.matrixClient.getRoom(ev.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
if (ev.getType() === EventType.SpaceChild && room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
this.emit(room.roomId);
|
||||
} else if (ev.getType() === EventType.SpaceParent) {
|
||||
// TODO rebuild the space parent and not the room - check permissions?
|
||||
// TODO confirm this after implementing parenting behaviour
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
} else {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
this.emit(room.roomId);
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => {
|
||||
if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
|
||||
// If the room was in favourites and now isn't or the opposite then update its position in the trees
|
||||
if (!!ev.getContent()[DefaultTagID.Favourite] !== !!lastEvent.getContent()[DefaultTagID.Favourite]) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
|
||||
if (ev.getType() === EventType.Direct) {
|
||||
const lastContent = lastEvent.getContent();
|
||||
const content = ev.getContent();
|
||||
|
||||
const diff = objectDiff<Record<string, string[]>>(lastContent, content);
|
||||
// filter out keys which changed by reference only by checking whether the sets differ
|
||||
const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
|
||||
// DM tag changes, refresh relevant rooms
|
||||
new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
|
||||
const room = this.matrixClient?.getRoom(roomId);
|
||||
if (room) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
protected async onNotReady() {
|
||||
if (!SettingsStore.getValue("feature_spaces")) return;
|
||||
if (this.matrixClient) {
|
||||
this.matrixClient.removeListener("Room", this.onRoom);
|
||||
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
||||
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
await this.reset({});
|
||||
}
|
||||
|
||||
protected async onReady() {
|
||||
if (!SettingsStore.getValue("feature_spaces")) return;
|
||||
this.matrixClient.on("Room", this.onRoom);
|
||||
this.matrixClient.on("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.on("RoomState.events", this.onRoomState);
|
||||
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.on("accountData", this.onAccountData);
|
||||
|
||||
await this.onSpaceUpdate(); // trigger an initial update
|
||||
|
||||
// restore selected state from last session if any and still valid
|
||||
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY);
|
||||
if (lastSpaceId) {
|
||||
const space = this.rootSpaces.find(s => s.roomId === lastSpaceId);
|
||||
if (space) {
|
||||
this.setActiveSpace(space);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
switch (payload.action) {
|
||||
case "view_room": {
|
||||
const room = this.matrixClient?.getRoom(payload.room_id);
|
||||
|
||||
if (room?.getMyMembership() === "join") {
|
||||
if (room.isSpaceRoom()) {
|
||||
this.setActiveSpace(room);
|
||||
} else if (!this.spaceFilteredRooms.get(this._activeSpace?.roomId || HOME_SPACE).has(room.roomId)) {
|
||||
// TODO maybe reverse these first 2 clauses once space panel active is fixed
|
||||
let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId));
|
||||
if (!parent) {
|
||||
parent = this.getCanonicalParent(room.roomId);
|
||||
}
|
||||
if (!parent) {
|
||||
const parents = Array.from(this.parentMap.get(room.roomId) || []);
|
||||
parent = parents.find(p => this.matrixClient.getRoom(p));
|
||||
}
|
||||
if (parent) {
|
||||
this.setActiveSpace(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "after_leave_room":
|
||||
if (this._activeSpace && payload.room_id === this._activeSpace.roomId) {
|
||||
this.setActiveSpace(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public getNotificationState(key: SpaceKey): SpaceNotificationState {
|
||||
if (this.notificationStateMap.has(key)) {
|
||||
return this.notificationStateMap.get(key);
|
||||
}
|
||||
|
||||
const state = new SpaceNotificationState(key, getRoomFn);
|
||||
this.notificationStateMap.set(key, state);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default class SpaceStore {
|
||||
private static internalInstance = new SpaceStoreClass();
|
||||
|
||||
public static get instance(): SpaceStoreClass {
|
||||
return SpaceStore.internalInstance;
|
||||
}
|
||||
}
|
||||
|
||||
window.mxSpaceStore = SpaceStore.instance;
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { NotificationColor } from "./NotificationColor";
|
||||
import { arrayDiff } from "../../utils/arrays";
|
||||
import { RoomNotificationState } from "./RoomNotificationState";
|
||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState";
|
||||
import { FetchRoomFn } from "./ListNotificationState";
|
||||
|
||||
export class SpaceNotificationState extends NotificationState {
|
||||
private rooms: Room[] = [];
|
||||
private states: { [spaceId: string]: RoomNotificationState } = {};
|
||||
|
||||
constructor(private spaceId: string | symbol, private getRoomFn: FetchRoomFn) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get symbol(): string {
|
||||
return null; // This notification state doesn't support symbols
|
||||
}
|
||||
|
||||
public setRooms(rooms: Room[]) {
|
||||
const oldRooms = this.rooms;
|
||||
const diff = arrayDiff(oldRooms, rooms);
|
||||
this.rooms = rooms;
|
||||
for (const oldRoom of diff.removed) {
|
||||
const state = this.states[oldRoom.roomId];
|
||||
if (!state) continue; // We likely just didn't have a badge (race condition)
|
||||
delete this.states[oldRoom.roomId];
|
||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
}
|
||||
for (const newRoom of diff.added) {
|
||||
const state = this.getRoomFn(newRoom);
|
||||
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
this.states[newRoom.roomId] = state;
|
||||
}
|
||||
|
||||
this.calculateTotalState();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
for (const state of Object.values(this.states)) {
|
||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
}
|
||||
this.states = {};
|
||||
}
|
||||
|
||||
private onRoomNotificationStateUpdate = () => {
|
||||
this.calculateTotalState();
|
||||
};
|
||||
|
||||
private calculateTotalState() {
|
||||
const snapshot = this.snapshot();
|
||||
|
||||
this._count = 0;
|
||||
this._color = NotificationColor.None;
|
||||
for (const state of Object.values(this.states)) {
|
||||
this._count += state.count;
|
||||
this._color = Math.max(this.color, state.color);
|
||||
}
|
||||
|
||||
// finally, publish an update if needed
|
||||
this.emitIfUpdated(snapshot);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
|||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
||||
import { SpaceWatcher } from "./SpaceWatcher";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
@ -56,7 +57,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
private initialListsGenerated = false;
|
||||
private algorithm = new Algorithm();
|
||||
private filterConditions: IFilterCondition[] = [];
|
||||
private tagWatcher = new TagWatcher(this);
|
||||
private tagWatcher: TagWatcher;
|
||||
private spaceWatcher: SpaceWatcher;
|
||||
private updateFn = new MarkedExecution(() => {
|
||||
for (const tagId of Object.keys(this.orderedLists)) {
|
||||
RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.orderedLists[tagId]);
|
||||
|
@ -77,6 +79,15 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
RoomViewStore.addListener(() => this.handleRVSUpdate({}));
|
||||
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
|
||||
this.setupWatchers();
|
||||
}
|
||||
|
||||
private setupWatchers() {
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
this.spaceWatcher = new SpaceWatcher(this);
|
||||
} else {
|
||||
this.tagWatcher = new TagWatcher(this);
|
||||
}
|
||||
}
|
||||
|
||||
public get unfilteredLists(): ITagMap {
|
||||
|
@ -92,9 +103,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// Intended for test usage
|
||||
public async resetStore() {
|
||||
await this.reset();
|
||||
this.tagWatcher = new TagWatcher(this);
|
||||
this.filterConditions = [];
|
||||
this.initialListsGenerated = false;
|
||||
this.setupWatchers();
|
||||
|
||||
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||
this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
|
||||
|
@ -554,8 +565,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
public async regenerateAllLists({trigger = true}) {
|
||||
console.warn("Regenerating all room lists");
|
||||
|
||||
const rooms = this.matrixClient.getVisibleRooms()
|
||||
.filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
||||
const rooms = [
|
||||
...this.matrixClient.getVisibleRooms(),
|
||||
// also show space invites in the room list
|
||||
...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
|
||||
].filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
||||
|
||||
const customTags = new Set<TagID>();
|
||||
if (this.state.tagsEnabled) {
|
||||
for (const room of rooms) {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { RoomListStoreClass } from "./RoomListStore";
|
||||
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
|
||||
|
||||
/**
|
||||
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
||||
*/
|
||||
export class SpaceWatcher {
|
||||
private filter = new SpaceFilterCondition();
|
||||
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
||||
|
||||
constructor(private store: RoomListStoreClass) {
|
||||
this.filter.updateSpace(this.activeSpace); // get the filter into a consistent state
|
||||
store.addFilter(this.filter);
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
||||
}
|
||||
|
||||
private onSelectedSpaceUpdated = (activeSpace) => {
|
||||
this.filter.updateSpace(this.activeSpace = activeSpace);
|
||||
};
|
||||
}
|
|
@ -186,6 +186,9 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
|
||||
private async doUpdateStickyRoom(val: Room) {
|
||||
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
||||
if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null;
|
||||
|
||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||
// otherwise we risk duplicating rooms.
|
||||
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
|
||||
import { setHasDiff } from "../../../utils/sets";
|
||||
|
||||
/**
|
||||
* A filter condition for the room list which reveals rooms which
|
||||
* are a member of a given space or if no space is selected shows:
|
||||
* + Orphaned rooms (ones not in any space you are a part of)
|
||||
* + All DMs
|
||||
*/
|
||||
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
|
||||
private roomIds = new Set<Room>();
|
||||
private space: Room = null;
|
||||
|
||||
public get relativePriority(): FilterPriority {
|
||||
// Lowest priority so we can coarsely find rooms.
|
||||
return FilterPriority.Lowest;
|
||||
}
|
||||
|
||||
public isVisible(room: Room): boolean {
|
||||
return this.roomIds.has(room.roomId);
|
||||
}
|
||||
|
||||
private onStoreUpdate = async (): Promise<void> => {
|
||||
const beforeRoomIds = this.roomIds;
|
||||
this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
|
||||
|
||||
if (setHasDiff(beforeRoomIds, this.roomIds)) {
|
||||
// XXX: Room List Store has a bug where rooms which are synced after the filter is set
|
||||
// are excluded from the filter, this is a workaround for it.
|
||||
this.emit(FILTER_CHANGED);
|
||||
setTimeout(() => {
|
||||
this.emit(FILTER_CHANGED);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
|
||||
|
||||
public updateSpace(space: Room) {
|
||||
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
|
||||
this.onStoreUpdate(); // initial update from the change to the space
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||
}
|
||||
}
|
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import isIp from "is-ip";
|
||||
import * as utils from 'matrix-js-sdk/src/utils';
|
||||
import * as utils from "matrix-js-sdk/src/utils";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
|
||||
import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
|
||||
import ElementPermalinkConstructor from "./ElementPermalinkConstructor";
|
||||
|
@ -121,6 +123,10 @@ export class RoomPermalinkCreator {
|
|||
this._started = false;
|
||||
}
|
||||
|
||||
get serverCandidates() {
|
||||
return this._serverCandidates;
|
||||
}
|
||||
|
||||
isStarted() {
|
||||
return this._started;
|
||||
}
|
||||
|
@ -451,3 +457,9 @@ function isHostnameIpAddress(hostname) {
|
|||
|
||||
return isIp(hostname);
|
||||
}
|
||||
|
||||
export const calculateRoomVia = (room: Room) => {
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.load();
|
||||
return permalinkCreator.serverCandidates;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import {calculateRoomVia} from "../utils/permalinks/Permalinks";
|
||||
|
||||
export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
|
||||
const userId = cli.getUserId();
|
||||
return space.getMyMembership() === "join"
|
||||
&& (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomName, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomTopic, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId));
|
||||
};
|
||||
|
||||
export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
|
||||
type: EventType.SpaceParent,
|
||||
content: {
|
||||
"via": calculateRoomVia(room),
|
||||
"canonical": canonical,
|
||||
},
|
||||
state_key: room.roomId,
|
||||
});
|
|
@ -5573,8 +5573,8 @@ mathml-tag-names@^2.1.3:
|
|||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "9.7.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c82bc35202f93efa2cb9b27b140f83df37c64ab2"
|
||||
version "9.8.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fb73ab687826e4d05fb8b424ab013a771213f84f"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
another-json "^0.2.0"
|
||||
|
|
Loading…
Reference in New Issue