Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/eslint1
Conflicts: src/components/views/dialogs/CreateRoomDialog.tsx src/components/views/messages/MImageBody.tsx
This commit is contained in:
commit
5ccd02dd6e
47 changed files with 1524 additions and 632 deletions
|
@ -86,6 +86,7 @@
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
@import "./views/dialogs/_InviteDialog.scss";
|
@import "./views/dialogs/_InviteDialog.scss";
|
||||||
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
||||||
|
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
|
||||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||||
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
||||||
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
||||||
|
|
|
@ -45,9 +45,14 @@ limitations under the License.
|
||||||
|
|
||||||
/* Overrides for the attachment body tiles */
|
/* Overrides for the attachment body tiles */
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile {
|
.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
margin-top: 32px;
|
margin-top: 10px;
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile .mx_MImageBody {
|
.mx_FilePanel .mx_EventTile .mx_MImageBody {
|
||||||
|
|
|
@ -84,7 +84,7 @@ limitations under the License.
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails {
|
||||||
padding-left: 36px; // align with the room name
|
padding-left: 36px; // align with the room name
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ limitations under the License.
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_line {
|
.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
padding-left: 36px; // align with the room name
|
padding-left: 36px; // align with the room name
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
|
|
@ -65,7 +65,7 @@ limitations under the License.
|
||||||
.mx_CreateRoomDialog_aliasContainer {
|
.mx_CreateRoomDialog_aliasContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
// put margin on container so it can collapse with siblings
|
// put margin on container so it can collapse with siblings
|
||||||
margin: 10px 0;
|
margin: 24px 0 10px;
|
||||||
|
|
||||||
.mx_RoomAliasField {
|
.mx_RoomAliasField {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -101,10 +101,6 @@ limitations under the License.
|
||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateRoomDialog_topic {
|
|
||||||
margin-bottom: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Dialog_content > .mx_SettingsFlag {
|
.mx_Dialog_content > .mx_SettingsFlag {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
@ -113,5 +109,56 @@ limitations under the License.
|
||||||
margin: 0 85px 0 0;
|
margin: 0 85px 0 0;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Dropdown {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-family: $font-family;
|
||||||
|
font-size: $font-14px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
.mx_Dropdown_input {
|
||||||
|
border: 1px solid $input-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dropdown_option {
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-32px;
|
||||||
|
height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding-left: 30px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 6px;
|
||||||
|
top: 8px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog_dropdown_invite::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/lock.svg');
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog_dropdown_public::before {
|
||||||
|
mask-image: url('$(res)/img/globe.svg');
|
||||||
|
mask-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog_dropdown_restricted::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,10 @@ limitations under the License.
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
150
res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
Normal file
150
res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
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_ManageRestrictedJoinRuleDialog_wrapper {
|
||||||
|
.mx_Dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog {
|
||||||
|
width: 480px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-height: 0;
|
||||||
|
height: 60vh;
|
||||||
|
|
||||||
|
.mx_SearchBox {
|
||||||
|
// To match the space around the title
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_noResults {
|
||||||
|
display: block;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_section {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_entry {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.mx_RoomAvatar_isSpaceRoom,
|
||||||
|
.mx_RoomAvatar_isSpaceRoom img {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_entry_name {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: 30px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_entry_description {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Checkbox {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_section_spaces {
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseAvatar_image {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_section_info {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 8px 8px 8px 42px;
|
||||||
|
background-color: $header-panel-bg-color;
|
||||||
|
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: calc(50% - 8px); // vertical centering
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_footer_buttons {
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
& + .mx_AccessibleButton {
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
border: 1px solid $strong-input-border-color;
|
border: 1px solid $strong-input-border-color;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
border: 1px solid $input-focused-border-color;
|
border: 1px solid $input-focused-border-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding-left: 10px;
|
padding: 0 10px;
|
||||||
border-left: 2px solid $button-bg-color;
|
border-left: 2px solid $button-bg-color;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,8 @@ limitations under the License.
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&.mx_EventTile_selected {
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -97,6 +98,11 @@ limitations under the License.
|
||||||
.mx_SenderProfile {
|
.mx_SenderProfile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_ReplyTile .mx_SenderProfile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_ReactionsRow {
|
||||||
float: right;
|
float: right;
|
||||||
clear: right;
|
clear: right;
|
||||||
|
|
|
@ -132,7 +132,8 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_EventTile_info .mx_EventTile_line {
|
&.mx_EventTile_info .mx_EventTile_line,
|
||||||
|
& ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line {
|
||||||
padding-left: calc($left-gutter + 18px);
|
padding-left: calc($left-gutter + 18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ $left-gutter: 64px;
|
||||||
|
|
||||||
> .mx_EventTile_avatar {
|
> .mx_EventTile_avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
|
|
|
@ -34,7 +34,7 @@ limitations under the License.
|
||||||
.mx_LinkPreviewWidget_caption {
|
.mx_LinkPreviewWidget_caption {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-x: hidden; // cause it to wrap rather than clip
|
overflow: hidden; // cause it to wrap rather than clip
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LinkPreviewWidget_title {
|
.mx_LinkPreviewWidget_title {
|
||||||
|
|
|
@ -47,14 +47,14 @@ limitations under the License.
|
||||||
color: $settings-subsection-fg-color;
|
color: $settings-subsection-fg-color;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 10px 100px 10px 0; // Align with the rest of the view
|
margin: 10px 80px 10px 0; // Align with the rest of the view
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsTab_section {
|
.mx_SettingsTab_section {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.mx_SettingsFlag {
|
.mx_SettingsFlag {
|
||||||
margin-right: 100px;
|
margin-right: 80px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab {
|
||||||
|
.mx_SettingsTab_showAdvanced {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab_spacesWithAccess {
|
||||||
|
> h4 {
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: 32px; // matches height of avatar for v-align
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
img.mx_RoomAvatar_isSpaceRoom,
|
||||||
|
.mx_RoomAvatar_isSpaceRoom img {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + span {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SecurityRoomSettingsTab_warning {
|
.mx_SecurityRoomSettingsTab_warning {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
@ -26,5 +64,51 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SecurityRoomSettingsTab_encryptionSection {
|
.mx_SecurityRoomSettingsTab_encryptionSection {
|
||||||
margin-bottom: 25px;
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid $menu-border-color;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab_upgradeRequired {
|
||||||
|
margin-left: 16px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
border: 1px solid $accent-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: $accent-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab_joinRule {
|
||||||
|
.mx_RadioButton {
|
||||||
|
padding-top: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.mx_RadioButton_content {
|
||||||
|
margin-left: 14px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 34px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
|
& + .mx_RadioButton {
|
||||||
|
border-top: 1px solid $menu-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g style="stroke:#454545;stroke-width:.8;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)">
|
|
||||||
<circle cx="5" cy="5" r="5"/>
|
|
||||||
<path d="m0 5h10"/>
|
|
||||||
<path d="m5 0c1.25064019 1.36917645 1.96137638 3.14601693 2 5-.03862362 1.85398307-.74935981 3.63082355-2 5-1.25064019-1.36917645-1.96137638-3.14601693-2-5 .03862362-1.85398307.74935981-3.63082355 2-5z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 524 B |
|
@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress';
|
||||||
import { abbreviateUrl } from './utils/UrlUtils';
|
import { abbreviateUrl } from './utils/UrlUtils';
|
||||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
||||||
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
|
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
|
||||||
import { inviteUsersToRoom } from "./RoomInvite";
|
|
||||||
import { WidgetType } from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import { Jitsi } from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
||||||
|
@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature";
|
||||||
import { CHAT_EFFECTS } from "./effects";
|
import { CHAT_EFFECTS } from "./effects";
|
||||||
import CallHandler from "./CallHandler";
|
import CallHandler from "./CallHandler";
|
||||||
import { guessAndSetDMRoom } from "./Rooms";
|
import { guessAndSetDMRoom } from "./Rooms";
|
||||||
|
import { upgradeRoom } from './utils/RoomUpgrade';
|
||||||
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
||||||
import ErrorDialog from './components/views/dialogs/ErrorDialog';
|
import ErrorDialog from './components/views/dialogs/ErrorDialog';
|
||||||
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
|
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
|
||||||
|
@ -277,50 +277,8 @@ export const Commands = [
|
||||||
/*isPriority=*/false, /*isStatic=*/true);
|
/*isPriority=*/false, /*isStatic=*/true);
|
||||||
|
|
||||||
return success(finished.then(async ([resp]) => {
|
return success(finished.then(async ([resp]) => {
|
||||||
if (!resp.continue) return;
|
if (!resp?.continue) return;
|
||||||
|
await upgradeRoom(room, args, resp.invite);
|
||||||
let checkForUpgradeFn;
|
|
||||||
try {
|
|
||||||
const upgradePromise = cli.upgradeRoom(roomId, args);
|
|
||||||
|
|
||||||
// We have to wait for the js-sdk to give us the room back so
|
|
||||||
// we can more effectively abuse the MultiInviter behaviour
|
|
||||||
// which heavily relies on the Room object being available.
|
|
||||||
if (resp.invite) {
|
|
||||||
checkForUpgradeFn = async (newRoom) => {
|
|
||||||
// The upgradePromise should be done by the time we await it here.
|
|
||||||
const { replacement_room: newRoomId } = await upgradePromise;
|
|
||||||
if (newRoom.roomId !== newRoomId) return;
|
|
||||||
|
|
||||||
const toInvite = [
|
|
||||||
...room.getMembersWithMembership("join"),
|
|
||||||
...room.getMembersWithMembership("invite"),
|
|
||||||
].map(m => m.userId).filter(m => m !== cli.getUserId());
|
|
||||||
|
|
||||||
if (toInvite.length > 0) {
|
|
||||||
// Errors are handled internally to this function
|
|
||||||
await inviteUsersToRoom(newRoomId, toInvite);
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.removeListener('Room', checkForUpgradeFn);
|
|
||||||
};
|
|
||||||
cli.on('Room', checkForUpgradeFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have to await after so that the checkForUpgradesFn has a proper reference
|
|
||||||
// to the new room's ID.
|
|
||||||
await upgradePromise;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
|
|
||||||
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
|
|
||||||
|
|
||||||
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
|
|
||||||
title: _t('Error upgrading room'),
|
|
||||||
description: _t(
|
|
||||||
'Double check that your server supports the room version chosen and try again.'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
|
|
@ -36,6 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { TileShape } from '../views/rooms/EventTile';
|
import { TileShape } from '../views/rooms/EventTile';
|
||||||
|
import { Layout } from "../../settings/Layout";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -267,6 +268,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
tileShape={TileShape.FileGrid}
|
tileShape={TileShape.FileGrid}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
empty={emptyState}
|
empty={emptyState}
|
||||||
|
layout={Layout.Group}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { TileShape } from "../views/rooms/EventTile";
|
import { TileShape } from "../views/rooms/EventTile";
|
||||||
|
import { Layout } from "../../settings/Layout";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
|
@ -52,6 +53,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
tileShape={TileShape.Notif}
|
tileShape={TileShape.Notif}
|
||||||
empty={emptyState}
|
empty={emptyState}
|
||||||
alwaysShowTimestamps={true}
|
alwaysShowTimestamps={true}
|
||||||
|
layout={Layout.Group}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { RefObject, useContext, useRef, useState } from "react";
|
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { EventSubscription } from "fbemitter";
|
import { EventSubscription } from "fbemitter";
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ import Modal from "../../Modal";
|
||||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||||
import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -309,7 +308,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
|
@ -332,7 +330,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|
||||||
if (await showCreateNewRoom(cli, space)) {
|
if (await showCreateNewRoom(space)) {
|
||||||
onNewRoomAdded();
|
onNewRoomAdded();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -345,7 +343,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|
||||||
const [added] = await showAddExistingRooms(cli, space);
|
const [added] = await showAddExistingRooms(space);
|
||||||
if (added) {
|
if (added) {
|
||||||
onNewRoomAdded();
|
onNewRoomAdded();
|
||||||
}
|
}
|
||||||
|
@ -399,11 +397,11 @@ const SpaceLanding = ({ space }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let settingsButton;
|
let settingsButton;
|
||||||
if (shouldShowSpaceSettings(cli, space)) {
|
if (shouldShowSpaceSettings(space)) {
|
||||||
settingsButton = <AccessibleTooltipButton
|
settingsButton = <AccessibleTooltipButton
|
||||||
className="mx_SpaceRoomView_landing_settingsButton"
|
className="mx_SpaceRoomView_landing_settingsButton"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showSpaceSettings(cli, space);
|
showSpaceSettings(space);
|
||||||
}}
|
}}
|
||||||
title={_t("Settings")}
|
title={_t("Settings")}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -13,9 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import BaseAvatar from './BaseAvatar';
|
import BaseAvatar from './BaseAvatar';
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
|
@ -32,11 +34,14 @@ interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idNam
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
room?: Room;
|
room?: Room;
|
||||||
oobData?: IOOBData;
|
oobData?: IOOBData & {
|
||||||
|
roomId?: string;
|
||||||
|
};
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
resizeMethod?: ResizeMethod;
|
resizeMethod?: ResizeMethod;
|
||||||
viewAvatarOnClick?: boolean;
|
viewAvatarOnClick?: boolean;
|
||||||
|
className?: string;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,15 +134,19 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
|
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
||||||
|
|
||||||
const roomName = room ? room.name : oobData.name;
|
const roomName = room ? room.name : oobData.name;
|
||||||
// If the room is a DM, we use the other user's ID for the color hash
|
// If the room is a DM, we use the other user's ID for the color hash
|
||||||
// in order to match the room avatar with their avatar
|
// in order to match the room avatar with their avatar
|
||||||
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null;
|
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar {...otherProps}
|
<BaseAvatar
|
||||||
|
{...otherProps}
|
||||||
|
className={classNames(className, {
|
||||||
|
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
|
||||||
|
})}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
idName={idName}
|
idName={idName}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import React, { ReactNode, useContext, useMemo, useState } from "react";
|
import React, { ReactNode, useContext, useMemo, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -44,9 +43,8 @@ import EntityTile from "../rooms/EntityTile";
|
||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
|
||||||
space: Room;
|
space: Room;
|
||||||
onCreateRoomClick(cli: MatrixClient, space: Room): void;
|
onCreateRoomClick(space: Room): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Entry = ({ room, checked, onChange }) => {
|
const Entry = ({ room, checked, onChange }) => {
|
||||||
|
@ -301,7 +299,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onFinished }) => {
|
||||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
|
|
||||||
|
@ -350,13 +348,13 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
>
|
>
|
||||||
<MatrixClientContext.Provider value={cli}>
|
<MatrixClientContext.Provider value={space.client}>
|
||||||
<AddExistingToSpace
|
<AddExistingToSpace
|
||||||
space={space}
|
space={space}
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
footerPrompt={<>
|
footerPrompt={<>
|
||||||
<div>{ _t("Want to add a new room instead?") }</div>
|
<div>{ _t("Want to add a new room instead?") }</div>
|
||||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
<AccessibleButton onClick={() => onCreateRoomClick(space)} kind="link">
|
||||||
{ _t("Create a new room") }
|
{ _t("Create a new room") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>}
|
</>}
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import withValidation, { IFieldState } from '../elements/Validation';
|
import withValidation, { IFieldState } from '../elements/Validation';
|
||||||
|
@ -31,7 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import BaseDialog from "../dialogs/BaseDialog";
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
import Dropdown from "../elements/Dropdown";
|
||||||
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
defaultPublic?: boolean;
|
defaultPublic?: boolean;
|
||||||
|
@ -41,7 +43,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
isPublic: boolean;
|
joinRule: JoinRule;
|
||||||
isEncrypted: boolean;
|
isEncrypted: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
topic: string;
|
topic: string;
|
||||||
|
@ -54,15 +56,25 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
||||||
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
|
private readonly supportsRestricted: boolean;
|
||||||
private nameField = createRef<Field>();
|
private nameField = createRef<Field>();
|
||||||
private aliasField = createRef<RoomAliasField>();
|
private aliasField = createRef<RoomAliasField>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred;
|
||||||
|
|
||||||
|
let joinRule = JoinRule.Invite;
|
||||||
|
if (this.props.defaultPublic) {
|
||||||
|
joinRule = JoinRule.Public;
|
||||||
|
} else if (this.supportsRestricted) {
|
||||||
|
joinRule = JoinRule.Restricted;
|
||||||
|
}
|
||||||
|
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
this.state = {
|
this.state = {
|
||||||
isPublic: this.props.defaultPublic || false,
|
joinRule,
|
||||||
isEncrypted: privateShouldBeEncrypted(),
|
isEncrypted: privateShouldBeEncrypted(),
|
||||||
name: this.props.defaultName || "",
|
name: this.props.defaultName || "",
|
||||||
topic: "",
|
topic: "",
|
||||||
|
@ -81,13 +93,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
const opts: IOpts = {};
|
const opts: IOpts = {};
|
||||||
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||||
createOpts.name = this.state.name;
|
createOpts.name = this.state.name;
|
||||||
if (this.state.isPublic) {
|
|
||||||
|
if (this.state.joinRule === JoinRule.Public) {
|
||||||
createOpts.visibility = Visibility.Public;
|
createOpts.visibility = Visibility.Public;
|
||||||
createOpts.preset = Preset.PublicChat;
|
createOpts.preset = Preset.PublicChat;
|
||||||
opts.guestAccess = false;
|
opts.guestAccess = false;
|
||||||
const { alias } = this.state;
|
const { alias } = this.state;
|
||||||
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
||||||
|
} else {
|
||||||
|
// If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
|
||||||
|
opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.topic) {
|
if (this.state.topic) {
|
||||||
createOpts.topic = this.state.topic;
|
createOpts.topic = this.state.topic;
|
||||||
}
|
}
|
||||||
|
@ -95,22 +112,13 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
createOpts.creation_content = { 'm.federate': false };
|
createOpts.creation_content = { 'm.federate': false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.isPublic) {
|
|
||||||
if (this.state.canChangeEncryption) {
|
|
||||||
opts.encryption = this.state.isEncrypted;
|
|
||||||
} else {
|
|
||||||
// the server should automatically do this for us, but for safety
|
|
||||||
// we'll demand it too.
|
|
||||||
opts.encryption = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.parentSpace) {
|
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
|
||||||
opts.parentSpace = this.props.parentSpace;
|
opts.parentSpace = this.props.parentSpace;
|
||||||
|
opts.joinRule = JoinRule.Restricted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
|
@ -172,8 +180,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
this.setState({ topic: ev.target.value });
|
this.setState({ topic: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPublicChange = (isPublic: boolean) => {
|
private onJoinRuleChange = (joinRule: JoinRule) => {
|
||||||
this.setState({ isPublic });
|
this.setState({ joinRule });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEncryptedChange = (isEncrypted: boolean) => {
|
private onEncryptedChange = (isEncrypted: boolean) => {
|
||||||
|
@ -210,7 +218,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let aliasField;
|
let aliasField;
|
||||||
if (this.state.isPublic) {
|
if (this.state.joinRule === JoinRule.Public) {
|
||||||
const domain = MatrixClientPeg.get().getDomain();
|
const domain = MatrixClientPeg.get().getDomain();
|
||||||
aliasField = (
|
aliasField = (
|
||||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||||
|
@ -224,19 +232,46 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let publicPrivateLabel = <p>{ _t(
|
let publicPrivateLabel: JSX.Element;
|
||||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
|
||||||
"found and joined by anyone.",
|
|
||||||
) }</p>;
|
|
||||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
publicPrivateLabel = <p>{ _t(
|
publicPrivateLabel = <p>
|
||||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
{ _t(
|
||||||
"found and joined by anyone in this community.",
|
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
||||||
) }</p>;
|
"found and joined by anyone in this community.",
|
||||||
|
) }
|
||||||
|
</p>;
|
||||||
|
} else if (this.state.joinRule === JoinRule.Restricted) {
|
||||||
|
publicPrivateLabel = <p>
|
||||||
|
{ _t(
|
||||||
|
"Everyone in <SpaceName/> will be able to find and join this room.", {}, {
|
||||||
|
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ _t("You can change this at any time from room settings.") }
|
||||||
|
</p>;
|
||||||
|
} else if (this.state.joinRule === JoinRule.Public) {
|
||||||
|
publicPrivateLabel = <p>
|
||||||
|
{ _t(
|
||||||
|
"Anyone will be able to find and join this room, not just members of <SpaceName/>.", {}, {
|
||||||
|
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ _t("You can change this at any time from room settings.") }
|
||||||
|
</p>;
|
||||||
|
} else if (this.state.joinRule === JoinRule.Invite) {
|
||||||
|
publicPrivateLabel = <p>
|
||||||
|
{ _t(
|
||||||
|
"Only people invited will be able to find and join this room.",
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ _t("You can change this at any time from room settings.") }
|
||||||
|
</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let e2eeSection;
|
let e2eeSection;
|
||||||
if (!this.state.isPublic) {
|
if (this.state.joinRule !== JoinRule.Public) {
|
||||||
let microcopy;
|
let microcopy;
|
||||||
if (privateShouldBeEncrypted()) {
|
if (privateShouldBeEncrypted()) {
|
||||||
if (this.state.canChangeEncryption) {
|
if (this.state.canChangeEncryption) {
|
||||||
|
@ -273,17 +308,31 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
let title = _t("Create a room");
|
||||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||||
title = _t("Create a room in %(communityName)s", { communityName: name });
|
title = _t("Create a room in %(communityName)s", { communityName: name });
|
||||||
|
} else if (!this.props.parentSpace) {
|
||||||
|
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
<div key={JoinRule.Invite} className="mx_CreateRoomDialog_dropdown_invite">
|
||||||
|
{ _t("Private room (invite only)") }
|
||||||
|
</div>,
|
||||||
|
<div key={JoinRule.Public} className="mx_CreateRoomDialog_dropdown_public">
|
||||||
|
{ _t("Public room") }
|
||||||
|
</div>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.supportsRestricted) {
|
||||||
|
options.unshift(<div key={JoinRule.Restricted} className="mx_CreateRoomDialog_dropdown_restricted">
|
||||||
|
{ _t("Visible to space members") }
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
|
||||||
className="mx_CreateRoomDialog"
|
|
||||||
onFinished={this.props.onFinished}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<Field
|
<Field
|
||||||
|
@ -300,11 +349,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
value={this.state.topic}
|
value={this.state.topic}
|
||||||
className="mx_CreateRoomDialog_topic"
|
className="mx_CreateRoomDialog_topic"
|
||||||
/>
|
/>
|
||||||
<LabelledToggleSwitch
|
|
||||||
label={_t("Make this room public")}
|
<Dropdown
|
||||||
onChange={this.onPublicChange}
|
id="mx_CreateRoomDialog_typeDropdown"
|
||||||
value={this.state.isPublic}
|
className="mx_CreateRoomDialog_typeDropdown"
|
||||||
/>
|
onOptionChange={this.onJoinRuleChange}
|
||||||
|
menuWidth={448}
|
||||||
|
value={this.state.joinRule}
|
||||||
|
label={_t("Room visibility")}
|
||||||
|
>
|
||||||
|
{ options }
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
{ publicPrivateLabel }
|
{ publicPrivateLabel }
|
||||||
{ e2eeSection }
|
{ e2eeSection }
|
||||||
{ aliasField }
|
{ aliasField }
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2017 New Vector Ltd.
|
|
||||||
Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de>
|
Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de>
|
||||||
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,31 +15,31 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ReactNode, KeyboardEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
export default class InfoDialog extends React.Component {
|
import { _t } from '../../../languageHandler';
|
||||||
static propTypes = {
|
import * as sdk from '../../../index';
|
||||||
className: PropTypes.string,
|
import { IDialogProps } from "./IDialogProps";
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.node,
|
|
||||||
button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
|
||||||
onFinished: PropTypes.func,
|
|
||||||
hasCloseButton: PropTypes.bool,
|
|
||||||
onKeyDown: PropTypes.func,
|
|
||||||
fixedWidth: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
title?: string;
|
||||||
|
description?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
button?: boolean | string;
|
||||||
|
hasCloseButton?: boolean;
|
||||||
|
fixedWidth?: boolean;
|
||||||
|
onKeyDown?(event: KeyboardEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class InfoDialog extends React.Component<IProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
hasCloseButton: false,
|
hasCloseButton: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
onFinished = () => {
|
private onFinished = () => {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
192
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal file
192
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import SearchBox from "../../structures/SearchBox";
|
||||||
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
room: Room;
|
||||||
|
selected?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Entry = ({ room, checked, onChange }) => {
|
||||||
|
const localRoom = room instanceof Room;
|
||||||
|
|
||||||
|
let description;
|
||||||
|
if (localRoom) {
|
||||||
|
description = _t("%(count)s members", { count: room.getJoinedMemberCount() });
|
||||||
|
const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length;
|
||||||
|
if (numChildRooms > 0) {
|
||||||
|
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <label className="mx_ManageRestrictedJoinRuleDialog_entry">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{ localRoom
|
||||||
|
? <RoomAvatar room={room} height={20} width={20} />
|
||||||
|
: <RoomAvatar oobData={room} height={20} width={20} />
|
||||||
|
}
|
||||||
|
<span className="mx_ManageRestrictedJoinRuleDialog_entry_name">{ room.name }</span>
|
||||||
|
</div>
|
||||||
|
{ description && <div className="mx_ManageRestrictedJoinRuleDialog_entry_description">
|
||||||
|
{ description }
|
||||||
|
</div> }
|
||||||
|
</div>
|
||||||
|
<StyledCheckbox
|
||||||
|
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||||
|
checked={checked}
|
||||||
|
disabled={!onChange}
|
||||||
|
/>
|
||||||
|
</label>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [], onFinished }) => {
|
||||||
|
const cli = room.client;
|
||||||
|
const [newSelected, setNewSelected] = useState(new Set<string>(selected));
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const lcQuery = query.toLowerCase().trim();
|
||||||
|
|
||||||
|
const [spacesContainingRoom, otherEntries] = useMemo(() => {
|
||||||
|
const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom());
|
||||||
|
return [
|
||||||
|
spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)),
|
||||||
|
selected.map(roomId => {
|
||||||
|
const room = cli.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
return { roomId, name: roomId } as Room;
|
||||||
|
}
|
||||||
|
if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) {
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
}).filter(Boolean),
|
||||||
|
];
|
||||||
|
}, [cli, selected, room.roomId]);
|
||||||
|
|
||||||
|
const [filteredSpacesContainingRooms, filteredOtherEntries] = useMemo(() => [
|
||||||
|
spacesContainingRoom.filter(r => r.name.toLowerCase().includes(lcQuery)),
|
||||||
|
otherEntries.filter(r => r.name.toLowerCase().includes(lcQuery)),
|
||||||
|
], [spacesContainingRoom, otherEntries, lcQuery]);
|
||||||
|
|
||||||
|
const onChange = (checked: boolean, room: Room): void => {
|
||||||
|
if (checked) {
|
||||||
|
newSelected.add(room.roomId);
|
||||||
|
} else {
|
||||||
|
newSelected.delete(room.roomId);
|
||||||
|
}
|
||||||
|
setNewSelected(new Set(newSelected));
|
||||||
|
};
|
||||||
|
|
||||||
|
let inviteOnlyWarning;
|
||||||
|
if (newSelected.size < 1) {
|
||||||
|
inviteOnlyWarning = <div className="mx_ManageRestrictedJoinRuleDialog_section_info">
|
||||||
|
{ _t("You're removing all spaces. Access will default to invite only") }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseDialog
|
||||||
|
title={_t("Select spaces")}
|
||||||
|
className="mx_ManageRestrictedJoinRuleDialog"
|
||||||
|
onFinished={onFinished}
|
||||||
|
fixedWidth={false}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{ _t("Decide which spaces can access this room. " +
|
||||||
|
"If a space is selected, its members can find and join <RoomName/>.", {}, {
|
||||||
|
RoomName: () => <b>{ room.name }</b>,
|
||||||
|
}) }
|
||||||
|
</p>
|
||||||
|
<MatrixClientContext.Provider value={cli}>
|
||||||
|
<SearchBox
|
||||||
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={_t("Search spaces")}
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoComplete={true}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
||||||
|
{ filteredSpacesContainingRooms.length > 0 ? (
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_section">
|
||||||
|
<h3>{ _t("Spaces you know that contain this room") }</h3>
|
||||||
|
{ filteredSpacesContainingRooms.map(space => {
|
||||||
|
return <Entry
|
||||||
|
key={space.roomId}
|
||||||
|
room={space}
|
||||||
|
checked={newSelected.has(space.roomId)}
|
||||||
|
onChange={(checked: boolean) => {
|
||||||
|
onChange(checked, space);
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : undefined }
|
||||||
|
|
||||||
|
{ filteredOtherEntries.length > 0 ? (
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_section">
|
||||||
|
<h3>{ _t("Other spaces or rooms you might not know") }</h3>
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_section_info">
|
||||||
|
<div>{ _t("These are likely ones other room admins are a part of.") }</div>
|
||||||
|
</div>
|
||||||
|
{ filteredOtherEntries.map(space => {
|
||||||
|
return <Entry
|
||||||
|
key={space.roomId}
|
||||||
|
room={space}
|
||||||
|
checked={newSelected.has(space.roomId)}
|
||||||
|
onChange={(checked: boolean) => {
|
||||||
|
onChange(checked, space);
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{ filteredSpacesContainingRooms.length + filteredOtherEntries.length < 1
|
||||||
|
? <span className="mx_ManageRestrictedJoinRuleDialog_noResults">
|
||||||
|
{ _t("No results") }
|
||||||
|
</span>
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_footer">
|
||||||
|
{ inviteOnlyWarning }
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_footer_buttons">
|
||||||
|
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton kind="primary" onClick={() => onFinished(Array.from(newSelected))}>
|
||||||
|
{ _t("Confirm") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
|
</BaseDialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManageRestrictedJoinRuleDialog;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,19 +15,29 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import ErrorDialog from './ErrorDialog';
|
||||||
|
import DialogButtons from '../elements/DialogButtons';
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
busy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomUpgradeDialog")
|
@replaceableComponent("views.dialogs.RoomUpgradeDialog")
|
||||||
export default class RoomUpgradeDialog extends React.Component {
|
export default class RoomUpgradeDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private targetVersion: string;
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -35,20 +45,19 @@ export default class RoomUpgradeDialog extends React.Component {
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const recommended = await this.props.room.getRecommendedVersion();
|
const recommended = await this.props.room.getRecommendedVersion();
|
||||||
this._targetVersion = recommended.version;
|
this.targetVersion = recommended.version;
|
||||||
this.setState({ busy: false });
|
this.setState({ busy: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancelClick = () => {
|
private onCancelClick = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUpgradeClick = () => {
|
private onUpgradeClick = (): void => {
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => {
|
upgradeRoom(this.props.room, this.targetVersion, false, false).then(() => {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
|
||||||
title: _t("Failed to upgrade room"),
|
title: _t("Failed to upgrade room"),
|
||||||
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
|
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
|
||||||
|
@ -59,29 +68,22 @@ export default class RoomUpgradeDialog extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
buttons = <Spinner />;
|
buttons = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
buttons = <DialogButtons
|
buttons = <DialogButtons
|
||||||
primaryButton={_t(
|
primaryButton={_t('Upgrade this room to version %(version)s', { version: this.targetVersion })}
|
||||||
'Upgrade this room to version %(version)s',
|
|
||||||
{ version: this._targetVersion },
|
|
||||||
)}
|
|
||||||
primaryButtonClass="danger"
|
primaryButtonClass="danger"
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onPrimaryButtonClick={this._onUpgradeClick}
|
onPrimaryButtonClick={this.onUpgradeClick}
|
||||||
focus={this.props.focus}
|
onCancel={this.onCancelClick}
|
||||||
onCancel={this._onCancelClick}
|
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_RoomUpgradeDialog"
|
<BaseDialog
|
||||||
|
className="mx_RoomUpgradeDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Upgrade Room Version")}
|
title={_t("Upgrade Room Version")}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
|
@ -97,8 +99,10 @@ export default class RoomUpgradeDialog extends React.Component {
|
||||||
<ol>
|
<ol>
|
||||||
<li>{ _t("Create a new room with the same name, description and avatar") }</li>
|
<li>{ _t("Create a new room with the same name, description and avatar") }</li>
|
||||||
<li>{ _t("Update any local room aliases to point to the new room") }</li>
|
<li>{ _t("Update any local room aliases to point to the new room") }</li>
|
||||||
<li>{ _t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room") }</li>
|
<li>{ _t("Stop users from speaking in the old version of the room, " +
|
||||||
<li>{ _t("Put a link back to the old room at the start of the new room so people can see old messages") }</li>
|
"and post a message advising users to move to the new room") }</li>
|
||||||
|
<li>{ _t("Put a link back to the old room at the start of the new room " +
|
||||||
|
"so people can see old messages") }</li>
|
||||||
</ol>
|
</ol>
|
||||||
{ buttons }
|
{ buttons }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,73 +14,82 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import BugReportDialog from './BugReportDialog';
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
roomId: string;
|
||||||
|
targetVersion: string;
|
||||||
|
description?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
inviteUsersToNewRoom: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
|
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
|
||||||
export default class RoomUpgradeWarningDialog extends React.Component {
|
export default class RoomUpgradeWarningDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private readonly isPrivate: boolean;
|
||||||
onFinished: PropTypes.func.isRequired,
|
private readonly currentVersion: string;
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
targetVersion: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null;
|
const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
|
||||||
const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true;
|
this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
|
||||||
|
this.currentVersion = room?.getVersion() || "1";
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentVersion: room ? room.getVersion() : "1",
|
|
||||||
isPrivate,
|
|
||||||
inviteUsersToNewRoom: true,
|
inviteUsersToNewRoom: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onContinue = () => {
|
private onContinue = () => {
|
||||||
this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom });
|
this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancel = () => {
|
private onCancel = () => {
|
||||||
this.props.onFinished({ continue: false, invite: false });
|
this.props.onFinished({ continue: false, invite: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onInviteUsersToggle = (newVal) => {
|
private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => {
|
||||||
this.setState({ inviteUsersToNewRoom: newVal });
|
this.setState({ inviteUsersToNewRoom });
|
||||||
};
|
};
|
||||||
|
|
||||||
_openBugReportDialog = (e) => {
|
private openBugReportDialog = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
|
||||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
|
|
||||||
let inviteToggle = null;
|
let inviteToggle = null;
|
||||||
if (this.state.isPrivate) {
|
if (this.isPrivate) {
|
||||||
inviteToggle = (
|
inviteToggle = (
|
||||||
<LabelledToggleSwitch
|
<LabelledToggleSwitch
|
||||||
value={this.state.inviteUsersToNewRoom}
|
value={this.state.inviteUsersToNewRoom}
|
||||||
onChange={this._onInviteUsersToggle}
|
onChange={this.onInviteUsersToggle}
|
||||||
label={_t("Automatically invite users")} />
|
label={_t("Automatically invite members from this room to the new one")} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
const title = this.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
||||||
|
|
||||||
let bugReports = (
|
let bugReports = (
|
||||||
<p>
|
<p>
|
||||||
|
@ -101,7 +110,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"a": (sub) => {
|
"a": (sub) => {
|
||||||
return <a href='#' onClick={this._openBugReportDialog}>{ sub }</a>;
|
return <a href='#' onClick={this.openBugReportDialog}>{ sub }</a>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
|
@ -119,18 +128,26 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ this.props.description || _t(
|
||||||
"Upgrading a room is an advanced action and is usually recommended when a room " +
|
"Upgrading a room is an advanced action and is usually recommended when a room " +
|
||||||
"is unstable due to bugs, missing features or security vulnerabilities.",
|
"is unstable due to bugs, missing features or security vulnerabilities.",
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(
|
||||||
|
"<b>Please note upgrading will make a new version of the room</b>. " +
|
||||||
|
"All current messages will stay in this archived room.", {}, {
|
||||||
|
b: sub => <b>{ sub }</b>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
{ bugReports }
|
{ bugReports }
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
oldVersion: () => <code>{ this.state.currentVersion }</code>,
|
oldVersion: () => <code>{ this.currentVersion }</code>,
|
||||||
newVersion: () => <code>{ this.props.targetVersion }</code>,
|
newVersion: () => <code>{ this.props.targetVersion }</code>,
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
|
@ -139,9 +156,9 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Upgrade")}
|
primaryButton={_t("Upgrade")}
|
||||||
onPrimaryButtonClick={this._onContinue}
|
onPrimaryButtonClick={this.onContinue}
|
||||||
cancelButton={_t("Cancel")}
|
cancelButton={_t("Cancel")}
|
||||||
onCancel={this._onCancel}
|
onCancel={this.onCancel}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,34 +15,38 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
class MenuOption extends React.Component {
|
interface IMenuOptionProps {
|
||||||
constructor(props) {
|
children: ReactElement;
|
||||||
super(props);
|
highlighted?: boolean;
|
||||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
dropdownKey: string;
|
||||||
this._onClick = this._onClick.bind(this);
|
id?: string;
|
||||||
}
|
inputRef?: Ref<HTMLDivElement>;
|
||||||
|
onClick(dropdownKey: string): void;
|
||||||
|
onMouseEnter(dropdownKey: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuOption extends React.Component<IMenuOptionProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMouseEnter() {
|
private onMouseEnter = () => {
|
||||||
this.props.onMouseEnter(this.props.dropdownKey);
|
this.props.onMouseEnter(this.props.dropdownKey);
|
||||||
}
|
};
|
||||||
|
|
||||||
_onClick(e) {
|
private onClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.props.onClick(this.props.dropdownKey);
|
this.props.onClick(this.props.dropdownKey);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const optClasses = classnames({
|
const optClasses = classnames({
|
||||||
|
@ -54,8 +57,8 @@ class MenuOption extends React.Component {
|
||||||
return <div
|
return <div
|
||||||
id={this.props.id}
|
id={this.props.id}
|
||||||
className={optClasses}
|
className={optClasses}
|
||||||
onClick={this._onClick}
|
onClick={this.onClick}
|
||||||
onMouseEnter={this._onMouseEnter}
|
onMouseEnter={this.onMouseEnter}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={this.props.highlighted}
|
aria-selected={this.props.highlighted}
|
||||||
ref={this.props.inputRef}
|
ref={this.props.inputRef}
|
||||||
|
@ -65,91 +68,97 @@ class MenuOption extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuOption.propTypes = {
|
interface IProps {
|
||||||
children: PropTypes.oneOfType([
|
id: string;
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
// ARIA label
|
||||||
PropTypes.node,
|
label: string;
|
||||||
]),
|
value?: string;
|
||||||
highlighted: PropTypes.bool,
|
className?: string;
|
||||||
dropdownKey: PropTypes.string,
|
children: ReactElement[];
|
||||||
onClick: PropTypes.func.isRequired,
|
// negative for consistency with HTML
|
||||||
onMouseEnter: PropTypes.func.isRequired,
|
disabled?: boolean;
|
||||||
inputRef: PropTypes.any,
|
// The width that the dropdown should be. If specified,
|
||||||
};
|
// the dropped-down part of the menu will be set to this
|
||||||
|
// width.
|
||||||
|
menuWidth?: number;
|
||||||
|
searchEnabled?: boolean;
|
||||||
|
// Called when the selected option changes
|
||||||
|
onOptionChange(dropdownKey: string): void;
|
||||||
|
// Called when the value of the search field changes
|
||||||
|
onSearchChange?(query: string): void;
|
||||||
|
// Function that, given the key of an option, returns
|
||||||
|
// a node representing that option to be displayed in the
|
||||||
|
// box itself as the currently-selected option (ie. as
|
||||||
|
// opposed to in the actual dropped-down part). If
|
||||||
|
// unspecified, the appropriate child element is used as
|
||||||
|
// in the dropped-down menu.
|
||||||
|
getShortOption?(value: string): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
expanded: boolean;
|
||||||
|
highlightedOption: string | null;
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Reusable dropdown select control, akin to react-select,
|
* Reusable dropdown select control, akin to react-select,
|
||||||
* but somewhat simpler as react-select is 79KB of minified
|
* but somewhat simpler as react-select is 79KB of minified
|
||||||
* javascript.
|
* javascript.
|
||||||
*
|
|
||||||
* TODO: Port NetworkDropdown to use this.
|
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.elements.Dropdown")
|
@replaceableComponent("views.elements.Dropdown")
|
||||||
export default class Dropdown extends React.Component {
|
export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
private readonly buttonRef = createRef<HTMLDivElement>();
|
||||||
|
private dropdownRootElement: HTMLDivElement = null;
|
||||||
|
private ignoreEvent: MouseEvent = null;
|
||||||
|
private childrenByKey: Record<string, ReactNode> = {};
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.dropdownRootElement = null;
|
this.reindexChildren(this.props.children);
|
||||||
this.ignoreEvent = null;
|
|
||||||
|
|
||||||
this._onInputClick = this._onInputClick.bind(this);
|
const firstChild = React.Children.toArray(props.children)[0] as ReactElement;
|
||||||
this._onRootClick = this._onRootClick.bind(this);
|
|
||||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
|
||||||
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
|
||||||
this._onInputChange = this._onInputChange.bind(this);
|
|
||||||
this._collectRoot = this._collectRoot.bind(this);
|
|
||||||
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
|
||||||
this._setHighlightedOption = this._setHighlightedOption.bind(this);
|
|
||||||
|
|
||||||
this.inputTextBox = null;
|
|
||||||
|
|
||||||
this._reindexChildren(this.props.children);
|
|
||||||
|
|
||||||
const firstChild = React.Children.toArray(props.children)[0];
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// True if the menu is dropped-down
|
// True if the menu is dropped-down
|
||||||
expanded: false,
|
expanded: false,
|
||||||
// The key of the highlighted option
|
// The key of the highlighted option
|
||||||
// (the option that would become selected if you pressed enter)
|
// (the option that would become selected if you pressed enter)
|
||||||
highlightedOption: firstChild ? firstChild.key : null,
|
highlightedOption: firstChild ? firstChild.key as string : null,
|
||||||
// the current search query
|
// the current search query
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
|
||||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
|
||||||
this._button = createRef();
|
|
||||||
// Listen for all clicks on the document so we can close the
|
// Listen for all clicks on the document so we can close the
|
||||||
// menu when the user clicks somewhere else
|
// menu when the user clicks somewhere else
|
||||||
document.addEventListener('click', this._onDocumentClick, false);
|
document.addEventListener('click', this.onDocumentClick, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('click', this._onDocumentClick, false);
|
document.removeEventListener('click', this.onDocumentClick, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line
|
||||||
if (!nextProps.children || nextProps.children.length === 0) {
|
if (!nextProps.children || nextProps.children.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._reindexChildren(nextProps.children);
|
this.reindexChildren(nextProps.children);
|
||||||
const firstChild = nextProps.children[0];
|
const firstChild = nextProps.children[0];
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedOption: firstChild ? firstChild.key : null,
|
highlightedOption: firstChild ? firstChild.key : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_reindexChildren(children) {
|
private reindexChildren(children: ReactElement[]): void {
|
||||||
this.childrenByKey = {};
|
this.childrenByKey = {};
|
||||||
React.Children.forEach(children, (child) => {
|
React.Children.forEach(children, (child) => {
|
||||||
this.childrenByKey[child.key] = child;
|
this.childrenByKey[child.key] = child;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDocumentClick(ev) {
|
private onDocumentClick = (ev: MouseEvent) => {
|
||||||
// Close the dropdown if the user clicks anywhere that isn't
|
// Close the dropdown if the user clicks anywhere that isn't
|
||||||
// within our root element
|
// within our root element
|
||||||
if (ev !== this.ignoreEvent) {
|
if (ev !== this.ignoreEvent) {
|
||||||
|
@ -157,9 +166,9 @@ export default class Dropdown extends React.Component {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onRootClick(ev) {
|
private onRootClick = (ev: MouseEvent) => {
|
||||||
// This captures any clicks that happen within our elements,
|
// This captures any clicks that happen within our elements,
|
||||||
// such that we can then ignore them when they're seen by the
|
// such that we can then ignore them when they're seen by the
|
||||||
// click listener on the document handler, ie. not close the
|
// click listener on the document handler, ie. not close the
|
||||||
|
@ -167,9 +176,9 @@ export default class Dropdown extends React.Component {
|
||||||
// NB. We can't just stopPropagation() because then the event
|
// NB. We can't just stopPropagation() because then the event
|
||||||
// doesn't reach the React onClick().
|
// doesn't reach the React onClick().
|
||||||
this.ignoreEvent = ev;
|
this.ignoreEvent = ev;
|
||||||
}
|
};
|
||||||
|
|
||||||
_onInputClick(ev) {
|
private onInputClick = (ev: React.MouseEvent) => {
|
||||||
if (this.props.disabled) return;
|
if (this.props.disabled) return;
|
||||||
|
|
||||||
if (!this.state.expanded) {
|
if (!this.state.expanded) {
|
||||||
|
@ -178,24 +187,24 @@ export default class Dropdown extends React.Component {
|
||||||
});
|
});
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_close() {
|
private close() {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: false,
|
expanded: false,
|
||||||
});
|
});
|
||||||
// their focus was on the input, its getting unmounted, move it to the button
|
// their focus was on the input, its getting unmounted, move it to the button
|
||||||
if (this._button.current) {
|
if (this.buttonRef.current) {
|
||||||
this._button.current.focus();
|
this.buttonRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMenuOptionClick(dropdownKey) {
|
private onMenuOptionClick = (dropdownKey: string) => {
|
||||||
this._close();
|
this.close();
|
||||||
this.props.onOptionChange(dropdownKey);
|
this.props.onOptionChange(dropdownKey);
|
||||||
}
|
};
|
||||||
|
|
||||||
_onInputKeyDown = (e) => {
|
private onInputKeyDown = (e: React.KeyboardEvent) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
|
|
||||||
// These keys don't generate keypress events and so needs to be on keyup
|
// These keys don't generate keypress events and so needs to be on keyup
|
||||||
|
@ -204,16 +213,16 @@ export default class Dropdown extends React.Component {
|
||||||
this.props.onOptionChange(this.state.highlightedOption);
|
this.props.onOptionChange(this.state.highlightedOption);
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case Key.ESCAPE:
|
case Key.ESCAPE:
|
||||||
this._close();
|
this.close();
|
||||||
break;
|
break;
|
||||||
case Key.ARROW_DOWN:
|
case Key.ARROW_DOWN:
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedOption: this._nextOption(this.state.highlightedOption),
|
highlightedOption: this.nextOption(this.state.highlightedOption),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Key.ARROW_UP:
|
case Key.ARROW_UP:
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedOption: this._prevOption(this.state.highlightedOption),
|
highlightedOption: this.prevOption(this.state.highlightedOption),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -224,53 +233,46 @@ export default class Dropdown extends React.Component {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onInputChange(e) {
|
private onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchQuery: e.target.value,
|
searchQuery: e.currentTarget.value,
|
||||||
});
|
});
|
||||||
if (this.props.onSearchChange) {
|
if (this.props.onSearchChange) {
|
||||||
this.props.onSearchChange(e.target.value);
|
this.props.onSearchChange(e.currentTarget.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_collectRoot(e) {
|
private collectRoot = (e: HTMLDivElement) => {
|
||||||
if (this.dropdownRootElement) {
|
if (this.dropdownRootElement) {
|
||||||
this.dropdownRootElement.removeEventListener(
|
this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
|
||||||
'click', this._onRootClick, false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (e) {
|
if (e) {
|
||||||
e.addEventListener('click', this._onRootClick, false);
|
e.addEventListener('click', this.onRootClick, false);
|
||||||
}
|
}
|
||||||
this.dropdownRootElement = e;
|
this.dropdownRootElement = e;
|
||||||
}
|
};
|
||||||
|
|
||||||
_collectInputTextBox(e) {
|
private setHighlightedOption = (optionKey: string) => {
|
||||||
this.inputTextBox = e;
|
|
||||||
if (e) e.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
_setHighlightedOption(optionKey) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedOption: optionKey,
|
highlightedOption: optionKey,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_nextOption(optionKey) {
|
private nextOption(optionKey: string): string {
|
||||||
const keys = Object.keys(this.childrenByKey);
|
const keys = Object.keys(this.childrenByKey);
|
||||||
const index = keys.indexOf(optionKey);
|
const index = keys.indexOf(optionKey);
|
||||||
return keys[(index + 1) % keys.length];
|
return keys[(index + 1) % keys.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
_prevOption(optionKey) {
|
private prevOption(optionKey: string): string {
|
||||||
const keys = Object.keys(this.childrenByKey);
|
const keys = Object.keys(this.childrenByKey);
|
||||||
const index = keys.indexOf(optionKey);
|
const index = keys.indexOf(optionKey);
|
||||||
return keys[(index - 1) % keys.length];
|
return keys[(index - 1) % keys.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
_scrollIntoView(node) {
|
private scrollIntoView(node: Element) {
|
||||||
if (node) {
|
if (node) {
|
||||||
node.scrollIntoView({
|
node.scrollIntoView({
|
||||||
block: "nearest",
|
block: "nearest",
|
||||||
|
@ -279,18 +281,18 @@ export default class Dropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMenuOptions() {
|
private getMenuOptions() {
|
||||||
const options = React.Children.map(this.props.children, (child) => {
|
const options = React.Children.map(this.props.children, (child) => {
|
||||||
const highlighted = this.state.highlightedOption === child.key;
|
const highlighted = this.state.highlightedOption === child.key;
|
||||||
return (
|
return (
|
||||||
<MenuOption
|
<MenuOption
|
||||||
id={`${this.props.id}__${child.key}`}
|
id={`${this.props.id}__${child.key}`}
|
||||||
key={child.key}
|
key={child.key}
|
||||||
dropdownKey={child.key}
|
dropdownKey={child.key as string}
|
||||||
highlighted={highlighted}
|
highlighted={highlighted}
|
||||||
onMouseEnter={this._setHighlightedOption}
|
onMouseEnter={this.setHighlightedOption}
|
||||||
onClick={this._onMenuOptionClick}
|
onClick={this.onMenuOptionClick}
|
||||||
inputRef={highlighted ? this._scrollIntoView : undefined}
|
inputRef={highlighted ? this.scrollIntoView : undefined}
|
||||||
>
|
>
|
||||||
{ child }
|
{ child }
|
||||||
</MenuOption>
|
</MenuOption>
|
||||||
|
@ -307,7 +309,7 @@ export default class Dropdown extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
let currentValue;
|
let currentValue;
|
||||||
|
|
||||||
const menuStyle = {};
|
const menuStyle: CSSProperties = {};
|
||||||
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
||||||
|
|
||||||
let menu;
|
let menu;
|
||||||
|
@ -316,10 +318,10 @@ export default class Dropdown extends React.Component {
|
||||||
currentValue = (
|
currentValue = (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
autoFocus={true}
|
||||||
className="mx_Dropdown_option"
|
className="mx_Dropdown_option"
|
||||||
ref={this._collectInputTextBox}
|
onKeyDown={this.onInputKeyDown}
|
||||||
onKeyDown={this._onInputKeyDown}
|
onChange={this.onInputChange}
|
||||||
onChange={this._onInputChange}
|
|
||||||
value={this.state.searchQuery}
|
value={this.state.searchQuery}
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-autocomplete="list"
|
aria-autocomplete="list"
|
||||||
|
@ -332,7 +334,7 @@ export default class Dropdown extends React.Component {
|
||||||
}
|
}
|
||||||
menu = (
|
menu = (
|
||||||
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
|
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
|
||||||
{ this._getMenuOptions() }
|
{ this.getMenuOptions() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -356,14 +358,14 @@ export default class Dropdown extends React.Component {
|
||||||
|
|
||||||
// Note the menu sits inside the AccessibleButton div so it's anchored
|
// Note the menu sits inside the AccessibleButton div so it's anchored
|
||||||
// to the input, but overflows below it. The root contains both.
|
// to the input, but overflows below it. The root contains both.
|
||||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_Dropdown_input mx_no_textinput"
|
className="mx_Dropdown_input mx_no_textinput"
|
||||||
onClick={this._onInputClick}
|
onClick={this.onInputClick}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={this.state.expanded}
|
aria-expanded={this.state.expanded}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
inputRef={this._button}
|
inputRef={this.buttonRef}
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
aria-describedby={`${this.props.id}_value`}
|
aria-describedby={`${this.props.id}_value`}
|
||||||
>
|
>
|
||||||
|
@ -374,28 +376,3 @@ export default class Dropdown extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dropdown.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
// The width that the dropdown should be. If specified,
|
|
||||||
// the dropped-down part of the menu will be set to this
|
|
||||||
// width.
|
|
||||||
menuWidth: PropTypes.number,
|
|
||||||
// Called when the selected option changes
|
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
|
||||||
// Called when the value of the search field changes
|
|
||||||
onSearchChange: PropTypes.func,
|
|
||||||
searchEnabled: PropTypes.bool,
|
|
||||||
// Function that, given the key of an option, returns
|
|
||||||
// a node representing that option to be displayed in the
|
|
||||||
// box itself as the currently-selected option (ie. as
|
|
||||||
// opposed to in the actual dropped-down part). If
|
|
||||||
// unspecified, the appropriate child element is used as
|
|
||||||
// in the dropped-down menu.
|
|
||||||
getShortOption: PropTypes.func,
|
|
||||||
value: PropTypes.string,
|
|
||||||
// negative for consistency with HTML
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
// ARIA label
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
};
|
|
|
@ -160,41 +160,80 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private zoom(delta: number) {
|
private zoomDelta(delta: number, anchorX?: number, anchorY?: number) {
|
||||||
const newZoom = this.state.zoom + delta;
|
this.zoom(this.state.zoom + delta, anchorX, anchorY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private zoom(zoomLevel: number, anchorX?: number, anchorY?: number) {
|
||||||
|
const oldZoom = this.state.zoom;
|
||||||
|
const newZoom = Math.min(zoomLevel, this.state.maxZoom);
|
||||||
|
|
||||||
if (newZoom <= this.state.minZoom) {
|
if (newZoom <= this.state.minZoom) {
|
||||||
|
// Zoom out fully
|
||||||
this.setState({
|
this.setState({
|
||||||
zoom: this.state.minZoom,
|
zoom: this.state.minZoom,
|
||||||
translationX: 0,
|
translationX: 0,
|
||||||
translationY: 0,
|
translationY: 0,
|
||||||
});
|
});
|
||||||
return;
|
} else if (typeof anchorX !== "number" && typeof anchorY !== "number") {
|
||||||
}
|
// Zoom relative to the center of the view
|
||||||
if (newZoom >= this.state.maxZoom) {
|
this.setState({
|
||||||
this.setState({ zoom: this.state.maxZoom });
|
zoom: newZoom,
|
||||||
return;
|
translationX: this.state.translationX * newZoom / oldZoom,
|
||||||
}
|
translationY: this.state.translationY * newZoom / oldZoom,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Zoom relative to the given point on the image.
|
||||||
|
// First we need to figure out the offset of the anchor point
|
||||||
|
// relative to the center of the image, accounting for rotation.
|
||||||
|
let offsetX;
|
||||||
|
let offsetY;
|
||||||
|
// The modulo operator can return negative values for some
|
||||||
|
// rotations, so we have to do some extra work to normalize it
|
||||||
|
switch (((this.state.rotation % 360) + 360) % 360) {
|
||||||
|
case 0:
|
||||||
|
offsetX = this.image.current.clientWidth / 2 - anchorX;
|
||||||
|
offsetY = this.image.current.clientHeight / 2 - anchorY;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
offsetX = anchorY - this.image.current.clientHeight / 2;
|
||||||
|
offsetY = this.image.current.clientWidth / 2 - anchorX;
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
offsetX = anchorX - this.image.current.clientWidth / 2;
|
||||||
|
offsetY = anchorY - this.image.current.clientHeight / 2;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
offsetX = this.image.current.clientHeight / 2 - anchorY;
|
||||||
|
offsetY = anchorX - this.image.current.clientWidth / 2;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
// Apply the zoom and offset
|
||||||
zoom: newZoom,
|
this.setState({
|
||||||
});
|
zoom: newZoom,
|
||||||
|
translationX: this.state.translationX + (newZoom - oldZoom) * offsetX,
|
||||||
|
translationY: this.state.translationY + (newZoom - oldZoom) * offsetY,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWheel = (ev: WheelEvent) => {
|
private onWheel = (ev: WheelEvent) => {
|
||||||
ev.stopPropagation();
|
if (ev.target === this.image.current) {
|
||||||
ev.preventDefault();
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
const { deltaY } = normalizeWheelEvent(ev);
|
const { deltaY } = normalizeWheelEvent(ev);
|
||||||
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
|
// Zoom in on the point on the image targeted by the cursor
|
||||||
|
this.zoomDelta(-deltaY * ZOOM_COEFFICIENT, ev.offsetX, ev.offsetY);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onZoomInClick = () => {
|
private onZoomInClick = () => {
|
||||||
this.zoom(ZOOM_STEP);
|
this.zoomDelta(ZOOM_STEP);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onZoomOutClick = () => {
|
private onZoomOutClick = () => {
|
||||||
this.zoom(-ZOOM_STEP);
|
this.zoomDelta(-ZOOM_STEP);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onKeyDown = (ev: KeyboardEvent) => {
|
private onKeyDown = (ev: KeyboardEvent) => {
|
||||||
|
@ -259,7 +298,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Zoom in if we are completely zoomed out
|
// Zoom in if we are completely zoomed out
|
||||||
if (this.state.zoom === this.state.minZoom) {
|
if (this.state.zoom === this.state.minZoom) {
|
||||||
this.setState({ zoom: this.state.maxZoom });
|
this.zoom(this.state.maxZoom, ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,11 +328,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE &&
|
Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE &&
|
||||||
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
|
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
|
||||||
) {
|
) {
|
||||||
this.setState({
|
this.zoom(this.state.minZoom);
|
||||||
zoom: this.state.minZoom,
|
|
||||||
translationX: 0,
|
|
||||||
translationY: 0,
|
|
||||||
});
|
|
||||||
this.initX = 0;
|
this.initX = 0;
|
||||||
this.initY = 0;
|
this.initY = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import StyledRadioButton from "./StyledRadioButton";
|
import StyledRadioButton from "./StyledRadioButton";
|
||||||
|
|
||||||
interface IDefinition<T extends string> {
|
export interface IDefinition<T extends string> {
|
||||||
value: T;
|
value: T;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label: React.ReactChild;
|
label: ReactNode;
|
||||||
description?: React.ReactChild;
|
description?: ReactNode;
|
||||||
checked?: boolean; // If provided it will override the value comparison done in the group
|
checked?: boolean; // If provided it will override the value comparison done in the group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ function StyledRadioGroup<T extends string>({
|
||||||
checked={d.checked !== undefined ? d.checked : d.value === value}
|
checked={d.checked !== undefined ? d.checked : d.value === value}
|
||||||
name={name}
|
name={name}
|
||||||
value={d.value}
|
value={d.value}
|
||||||
disabled={disabled || d.disabled}
|
disabled={d.disabled ?? disabled}
|
||||||
outlined={outlined}
|
outlined={outlined}
|
||||||
>
|
>
|
||||||
{ d.label }
|
{ d.label }
|
||||||
|
|
|
@ -347,7 +347,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
className="mx_MImageBody_thumbnail"
|
className="mx_MImageBody_thumbnail"
|
||||||
src={thumbUrl}
|
src={thumbUrl}
|
||||||
ref={this.image}
|
ref={this.image}
|
||||||
style={{ maxWidth: maxWidth + "px" }}
|
style={{ maxWidth: `min(100%, ${maxWidth}px)` }}
|
||||||
alt={content.body}
|
alt={content.body}
|
||||||
onError={this.onImageError}
|
onError={this.onImageError}
|
||||||
onLoad={this.onImageLoad}
|
onLoad={this.onImageLoad}
|
||||||
|
@ -372,7 +372,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
className="mx_MImageBody_thumbnail"
|
className="mx_MImageBody_thumbnail"
|
||||||
style={{
|
style={{
|
||||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||||
maxWidth: infoWidth + "px",
|
maxWidth: `min(100%, ${infoWidth}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ placeholder }
|
{ placeholder }
|
||||||
|
@ -461,7 +461,7 @@ export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProp
|
||||||
let className = 'mx_HiddenImagePlaceholder';
|
let className = 'mx_HiddenImagePlaceholder';
|
||||||
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
|
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
|
||||||
return (
|
return (
|
||||||
<div className={className} style={{ maxWidth: maxWidth }}>
|
<div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
|
||||||
<div className='mx_HiddenImagePlaceholder_button'>
|
<div className='mx_HiddenImagePlaceholder_button'>
|
||||||
<span className='mx_HiddenImagePlaceholder_eye' />
|
<span className='mx_HiddenImagePlaceholder_eye' />
|
||||||
<span>{ _t("Show image") }</span>
|
<span>{ _t("Show image") }</span>
|
||||||
|
|
|
@ -514,7 +514,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
switch (content.msgtype) {
|
switch (content.msgtype) {
|
||||||
case MsgType.Emote:
|
case MsgType.Emote:
|
||||||
return (
|
return (
|
||||||
<span className="mx_MEmoteBody mx_EventTile_content">
|
<div className="mx_MEmoteBody mx_EventTile_content">
|
||||||
*
|
*
|
||||||
<span
|
<span
|
||||||
className="mx_MEmoteBody_sender"
|
className="mx_MEmoteBody_sender"
|
||||||
|
@ -525,21 +525,21 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
{ body }
|
{ body }
|
||||||
{ widgets }
|
{ widgets }
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
case MsgType.Notice:
|
case MsgType.Notice:
|
||||||
return (
|
return (
|
||||||
<span className="mx_MNoticeBody mx_EventTile_content">
|
<div className="mx_MNoticeBody mx_EventTile_content">
|
||||||
{ body }
|
{ body }
|
||||||
{ widgets }
|
{ widgets }
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
default: // including "m.text"
|
default: // including "m.text"
|
||||||
return (
|
return (
|
||||||
<span className="mx_MTextBody mx_EventTile_content">
|
<div className="mx_MTextBody mx_EventTile_content">
|
||||||
{ body }
|
{ body }
|
||||||
{ widgets }
|
{ widgets }
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1147,6 +1147,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
{ ircTimestamp }
|
{ ircTimestamp }
|
||||||
{ sender }
|
{ sender }
|
||||||
{ ircPadlock }
|
{ ircPadlock }
|
||||||
|
{ avatar }
|
||||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||||
{ groupTimestamp }
|
{ groupTimestamp }
|
||||||
{ groupPadlock }
|
{ groupPadlock }
|
||||||
|
@ -1167,7 +1168,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
{ reactionsRow }
|
{ reactionsRow }
|
||||||
{ msgOption }
|
{ msgOption }
|
||||||
{ avatar }
|
|
||||||
</>)
|
</>)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onFinished();
|
onFinished();
|
||||||
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
showCreateNewRoom(SpaceStore.instance.activeSpace);
|
||||||
}}
|
}}
|
||||||
disabled={!canAddRooms}
|
disabled={!canAddRooms}
|
||||||
tooltip={canAddRooms ? undefined
|
tooltip={canAddRooms ? undefined
|
||||||
|
@ -153,7 +153,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onFinished();
|
onFinished();
|
||||||
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
showAddExistingRooms(SpaceStore.instance.activeSpace);
|
||||||
}}
|
}}
|
||||||
disabled={!canAddRooms}
|
disabled={!canAddRooms}
|
||||||
tooltip={canAddRooms ? undefined
|
tooltip={canAddRooms ? undefined
|
||||||
|
|
|
@ -15,52 +15,43 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||||
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
|
import StyledRadioGroup, { IDefinition } from '../../../elements/StyledRadioGroup';
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
|
import SpaceStore from "../../../../../stores/SpaceStore";
|
||||||
|
import RoomAvatar from "../../../avatars/RoomAvatar";
|
||||||
|
import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog';
|
||||||
|
import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog';
|
||||||
|
import { upgradeRoom } from "../../../../../utils/RoomUpgrade";
|
||||||
|
import { arrayHasDiff } from "../../../../../utils/arrays";
|
||||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
|
|
||||||
// Knock and private are reserved keywords which are not yet implemented.
|
|
||||||
export enum JoinRule {
|
|
||||||
Public = "public",
|
|
||||||
Knock = "knock",
|
|
||||||
Invite = "invite",
|
|
||||||
/**
|
|
||||||
* @deprecated Reserved. Should not be used.
|
|
||||||
*/
|
|
||||||
Private = "private",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum GuestAccess {
|
|
||||||
CanJoin = "can_join",
|
|
||||||
Forbidden = "forbidden",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum HistoryVisibility {
|
|
||||||
Invited = "invited",
|
|
||||||
Joined = "joined",
|
|
||||||
Shared = "shared",
|
|
||||||
WorldReadable = "world_readable",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
joinRule: JoinRule;
|
joinRule: JoinRule;
|
||||||
|
restrictedAllowRoomIds?: string[];
|
||||||
guestAccess: GuestAccess;
|
guestAccess: GuestAccess;
|
||||||
history: HistoryVisibility;
|
history: HistoryVisibility;
|
||||||
hasAliases: boolean;
|
hasAliases: boolean;
|
||||||
encrypted: boolean;
|
encrypted: boolean;
|
||||||
|
roomSupportsRestricted?: boolean;
|
||||||
|
preferredRestrictionVersion?: string;
|
||||||
|
showAdvancedSection: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
|
@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
|
||||||
|
@ -70,44 +61,58 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
joinRule: JoinRule.Invite,
|
joinRule: JoinRule.Invite,
|
||||||
guestAccess: GuestAccess.CanJoin,
|
guestAccess: GuestAccess.Forbidden,
|
||||||
history: HistoryVisibility.Shared,
|
history: HistoryVisibility.Shared,
|
||||||
hasAliases: false,
|
hasAliases: false,
|
||||||
encrypted: false,
|
encrypted: false,
|
||||||
|
showAdvancedSection: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move this to constructor
|
// TODO: [REACT-WARNING] Move this to constructor
|
||||||
async UNSAFE_componentWillMount() { // eslint-disable-line
|
UNSAFE_componentWillMount() { // eslint-disable-line
|
||||||
MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
|
const cli = MatrixClientPeg.get();
|
||||||
|
cli.on("RoomState.events", this.onStateEvent);
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
const state = room.currentState;
|
const state = room.currentState;
|
||||||
|
|
||||||
const joinRule: JoinRule = this.pullContentPropertyFromEvent(
|
const joinRuleEvent = state.getStateEvents(EventType.RoomJoinRules, "");
|
||||||
state.getStateEvents("m.room.join_rules", ""),
|
const joinRule: JoinRule = this.pullContentPropertyFromEvent<JoinRule>(
|
||||||
|
joinRuleEvent,
|
||||||
'join_rule',
|
'join_rule',
|
||||||
JoinRule.Invite,
|
JoinRule.Invite,
|
||||||
);
|
);
|
||||||
const guestAccess: GuestAccess = this.pullContentPropertyFromEvent(
|
const restrictedAllowRoomIds = joinRule === JoinRule.Restricted
|
||||||
state.getStateEvents("m.room.guest_access", ""),
|
? joinRuleEvent?.getContent().allow
|
||||||
|
?.filter(a => a.type === RestrictedAllowType.RoomMembership)
|
||||||
|
?.map(a => a.room_id)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const guestAccess: GuestAccess = this.pullContentPropertyFromEvent<GuestAccess>(
|
||||||
|
state.getStateEvents(EventType.RoomGuestAccess, ""),
|
||||||
'guest_access',
|
'guest_access',
|
||||||
GuestAccess.Forbidden,
|
GuestAccess.Forbidden,
|
||||||
);
|
);
|
||||||
const history: HistoryVisibility = this.pullContentPropertyFromEvent(
|
const history: HistoryVisibility = this.pullContentPropertyFromEvent<HistoryVisibility>(
|
||||||
state.getStateEvents("m.room.history_visibility", ""),
|
state.getStateEvents(EventType.RoomHistoryVisibility, ""),
|
||||||
'history_visibility',
|
'history_visibility',
|
||||||
HistoryVisibility.Shared,
|
HistoryVisibility.Shared,
|
||||||
);
|
);
|
||||||
|
|
||||||
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||||
this.setState({ joinRule, guestAccess, history, encrypted });
|
const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport;
|
||||||
const hasAliases = await this.hasAliases();
|
const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support)
|
||||||
this.setState({ hasAliases });
|
&& restrictedRoomCapabilities.support.includes(room.getVersion());
|
||||||
|
const preferredRestrictionVersion = roomSupportsRestricted ? undefined : restrictedRoomCapabilities?.preferred;
|
||||||
|
this.setState({ joinRule, restrictedAllowRoomIds, guestAccess, history, encrypted,
|
||||||
|
roomSupportsRestricted, preferredRestrictionVersion });
|
||||||
|
|
||||||
|
this.hasAliases().then(hasAliases => this.setState({ hasAliases }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
|
private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
|
||||||
if (!event || !event.getContent()) return defaultValue;
|
return event?.getContent()[key] || defaultValue;
|
||||||
return event.getContent()[key] || defaultValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -115,13 +120,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStateEvent = (e: MatrixEvent) => {
|
private onStateEvent = (e: MatrixEvent) => {
|
||||||
const refreshWhenTypes = [
|
const refreshWhenTypes: EventType[] = [
|
||||||
'm.room.join_rules',
|
EventType.RoomJoinRules,
|
||||||
'm.room.guest_access',
|
EventType.RoomGuestAccess,
|
||||||
'm.room.history_visibility',
|
EventType.RoomHistoryVisibility,
|
||||||
'm.room.encryption',
|
EventType.RoomEncryption,
|
||||||
];
|
];
|
||||||
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
|
if (refreshWhenTypes.includes(e.getType() as EventType)) this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEncryptionChange = () => {
|
private onEncryptionChange = () => {
|
||||||
|
@ -149,7 +154,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
const beforeEncrypted = this.state.encrypted;
|
const beforeEncrypted = this.state.encrypted;
|
||||||
this.setState({ encrypted: true });
|
this.setState({ encrypted: true });
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
this.props.roomId, "m.room.encryption",
|
this.props.roomId, EventType.RoomEncryption,
|
||||||
{ algorithm: "m.megolm.v1.aes-sha2" },
|
{ algorithm: "m.megolm.v1.aes-sha2" },
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -159,89 +164,91 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private fixGuestAccess = (e: React.MouseEvent) => {
|
private onJoinRuleChange = async (joinRule: JoinRule) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const joinRule = JoinRule.Invite;
|
|
||||||
const guestAccess = GuestAccess.CanJoin;
|
|
||||||
|
|
||||||
const beforeJoinRule = this.state.joinRule;
|
const beforeJoinRule = this.state.joinRule;
|
||||||
const beforeGuestAccess = this.state.guestAccess;
|
|
||||||
this.setState({ joinRule, guestAccess });
|
let restrictedAllowRoomIds: string[];
|
||||||
|
if (joinRule === JoinRule.Restricted) {
|
||||||
|
const matrixClient = MatrixClientPeg.get();
|
||||||
|
const roomId = this.props.roomId;
|
||||||
|
const room = matrixClient.getRoom(roomId);
|
||||||
|
|
||||||
|
if (beforeJoinRule === JoinRule.Restricted || this.state.roomSupportsRestricted) {
|
||||||
|
// Have the user pick which spaces to allow joins from
|
||||||
|
restrictedAllowRoomIds = await this.editRestrictedRoomIds();
|
||||||
|
if (!Array.isArray(restrictedAllowRoomIds)) return;
|
||||||
|
} else if (this.state.preferredRestrictionVersion) {
|
||||||
|
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
||||||
|
const targetVersion = this.state.preferredRestrictionVersion;
|
||||||
|
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||||
|
roomId,
|
||||||
|
targetVersion,
|
||||||
|
description: _t("This upgrade will allow members of selected spaces " +
|
||||||
|
"access to this room without an invite."),
|
||||||
|
onFinished: (resp) => {
|
||||||
|
if (!resp?.continue) return;
|
||||||
|
upgradeRoom(room, targetVersion, resp.invite);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
|
||||||
|
|
||||||
|
const content: IContent = {
|
||||||
|
join_rule: joinRule,
|
||||||
|
};
|
||||||
|
|
||||||
|
// pre-set the accepted spaces with the currently viewed one as per the microcopy
|
||||||
|
if (joinRule === JoinRule.Restricted) {
|
||||||
|
content.allow = restrictedAllowRoomIds.map(roomId => ({
|
||||||
|
"type": RestrictedAllowType.RoomMembership,
|
||||||
|
"room_id": roomId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ joinRule, restrictedAllowRoomIds });
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
client.sendStateEvent(
|
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, content, "").catch((e) => {
|
||||||
this.props.roomId,
|
|
||||||
"m.room.join_rules",
|
|
||||||
{ join_rule: joinRule },
|
|
||||||
"",
|
|
||||||
).catch((e) => {
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
this.setState({ joinRule: beforeJoinRule });
|
this.setState({
|
||||||
});
|
joinRule: beforeJoinRule,
|
||||||
client.sendStateEvent(
|
restrictedAllowRoomIds: undefined,
|
||||||
this.props.roomId,
|
});
|
||||||
"m.room.guest_access",
|
|
||||||
{ guest_access: guestAccess },
|
|
||||||
"",
|
|
||||||
).catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
this.setState({ guestAccess: beforeGuestAccess });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomAccessRadioToggle = (roomAccess: string) => {
|
private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => {
|
||||||
// join_rule
|
const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds;
|
||||||
// INVITE | PUBLIC
|
if (!arrayHasDiff(beforeRestrictedAllowRoomIds || [], restrictedAllowRoomIds)) return;
|
||||||
// ----------------------+----------------
|
this.setState({ restrictedAllowRoomIds });
|
||||||
// guest CAN_JOIN | inv_only | pub_with_guest
|
|
||||||
// access ----------------------+----------------
|
|
||||||
// FORBIDDEN | inv_only | pub_no_guest
|
|
||||||
// ----------------------+----------------
|
|
||||||
|
|
||||||
// we always set guests can_join here as it makes no sense to have
|
|
||||||
// an invite-only room that guests can't join. If you explicitly
|
|
||||||
// invite them, you clearly want them to join, whether they're a
|
|
||||||
// guest or not. In practice, guest_access should probably have
|
|
||||||
// been implemented as part of the join_rules enum.
|
|
||||||
let joinRule = JoinRule.Invite;
|
|
||||||
let guestAccess = GuestAccess.CanJoin;
|
|
||||||
|
|
||||||
switch (roomAccess) {
|
|
||||||
case "invite_only":
|
|
||||||
// no change - use defaults above
|
|
||||||
break;
|
|
||||||
case "public_no_guests":
|
|
||||||
joinRule = JoinRule.Public;
|
|
||||||
guestAccess = GuestAccess.Forbidden;
|
|
||||||
break;
|
|
||||||
case "public_with_guests":
|
|
||||||
joinRule = JoinRule.Public;
|
|
||||||
guestAccess = GuestAccess.CanJoin;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeJoinRule = this.state.joinRule;
|
|
||||||
const beforeGuestAccess = this.state.guestAccess;
|
|
||||||
this.setState({ joinRule, guestAccess });
|
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
client.sendStateEvent(
|
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, {
|
||||||
this.props.roomId,
|
join_rule: JoinRule.Restricted,
|
||||||
"m.room.join_rules",
|
allow: restrictedAllowRoomIds.map(roomId => ({
|
||||||
{ join_rule: joinRule },
|
"type": RestrictedAllowType.RoomMembership,
|
||||||
"",
|
"room_id": roomId,
|
||||||
).catch((e) => {
|
})),
|
||||||
|
}, "").catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
this.setState({ joinRule: beforeJoinRule });
|
this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds });
|
||||||
});
|
});
|
||||||
client.sendStateEvent(
|
};
|
||||||
this.props.roomId,
|
|
||||||
"m.room.guest_access",
|
private onGuestAccessChange = (allowed: boolean) => {
|
||||||
{ guest_access: guestAccess },
|
const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
|
||||||
"",
|
const beforeGuestAccess = this.state.guestAccess;
|
||||||
).catch((e) => {
|
if (beforeGuestAccess === guestAccess) return;
|
||||||
|
|
||||||
|
this.setState({ guestAccess });
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
client.sendStateEvent(this.props.roomId, EventType.RoomGuestAccess, {
|
||||||
|
guest_access: guestAccess,
|
||||||
|
}, "").catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
this.setState({ guestAccess: beforeGuestAccess });
|
this.setState({ guestAccess: beforeGuestAccess });
|
||||||
});
|
});
|
||||||
|
@ -249,8 +256,10 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
|
|
||||||
private onHistoryRadioToggle = (history: HistoryVisibility) => {
|
private onHistoryRadioToggle = (history: HistoryVisibility) => {
|
||||||
const beforeHistory = this.state.history;
|
const beforeHistory = this.state.history;
|
||||||
|
if (beforeHistory === history) return;
|
||||||
|
|
||||||
this.setState({ history: history });
|
this.setState({ history: history });
|
||||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", {
|
MatrixClientPeg.get().sendStateEvent(this.props.roomId, EventType.RoomHistoryVisibility, {
|
||||||
history_visibility: history,
|
history_visibility: history,
|
||||||
}, "").catch((e) => {
|
}, "").catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -270,36 +279,48 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
return Array.isArray(localAliases) && localAliases.length !== 0;
|
return Array.isArray(localAliases) && localAliases.length !== 0;
|
||||||
} else {
|
} else {
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
|
const aliasEvents = room.currentState.getStateEvents(EventType.RoomAliases) || [];
|
||||||
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
|
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
|
||||||
return hasAliases;
|
return hasAliases;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderRoomAccess() {
|
private editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
|
||||||
|
let selected = this.state.restrictedAllowRoomIds;
|
||||||
|
if (!selected?.length && SpaceStore.instance.activeSpace) {
|
||||||
|
selected = [SpaceStore.instance.activeSpace.roomId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matrixClient = MatrixClientPeg.get();
|
||||||
|
const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, {
|
||||||
|
matrixClient,
|
||||||
|
room: matrixClient.getRoom(this.props.roomId),
|
||||||
|
selected,
|
||||||
|
}, "mx_ManageRestrictedJoinRuleDialog_wrapper");
|
||||||
|
|
||||||
|
const [restrictedAllowRoomIds] = await finished;
|
||||||
|
return restrictedAllowRoomIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEditRestrictedClick = async () => {
|
||||||
|
const restrictedAllowRoomIds = await this.editRestrictedRoomIds();
|
||||||
|
if (!Array.isArray(restrictedAllowRoomIds)) return;
|
||||||
|
if (restrictedAllowRoomIds.length > 0) {
|
||||||
|
this.onRestrictedRoomIdsChange(restrictedAllowRoomIds);
|
||||||
|
} else {
|
||||||
|
this.onJoinRuleChange(JoinRule.Invite);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderJoinRule() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const joinRule = this.state.joinRule;
|
const joinRule = this.state.joinRule;
|
||||||
const guestAccess = this.state.guestAccess;
|
|
||||||
|
|
||||||
const canChangeAccess = room.currentState.mayClientSendStateEvent("m.room.join_rules", client)
|
const canChangeJoinRule = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client);
|
||||||
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
|
|
||||||
|
|
||||||
let guestWarning = null;
|
|
||||||
if (joinRule !== 'public' && guestAccess === 'forbidden') {
|
|
||||||
guestWarning = (
|
|
||||||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
|
||||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
|
||||||
<span>
|
|
||||||
{ _t("Guests cannot join this room even if explicitly invited.") }
|
|
||||||
<a href="" onClick={this.fixGuestAccess}>{ _t("Click here to fix") }</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let aliasWarning = null;
|
let aliasWarning = null;
|
||||||
if (joinRule === 'public' && !this.state.hasAliases) {
|
if (joinRule === JoinRule.Public && !this.state.hasAliases) {
|
||||||
aliasWarning = (
|
aliasWarning = (
|
||||||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||||
|
@ -310,34 +331,107 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const radioDefinitions: IDefinition<JoinRule>[] = [{
|
||||||
|
value: JoinRule.Invite,
|
||||||
|
label: _t("Private (invite only)"),
|
||||||
|
description: _t("Only invited people can join."),
|
||||||
|
checked: this.state.joinRule === JoinRule.Invite
|
||||||
|
|| (this.state.joinRule === JoinRule.Restricted && !this.state.restrictedAllowRoomIds?.length),
|
||||||
|
}, {
|
||||||
|
value: JoinRule.Public,
|
||||||
|
label: _t("Public"),
|
||||||
|
description: _t("Anyone can find and join."),
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (this.state.roomSupportsRestricted ||
|
||||||
|
this.state.preferredRestrictionVersion ||
|
||||||
|
joinRule === JoinRule.Restricted
|
||||||
|
) {
|
||||||
|
let upgradeRequiredPill;
|
||||||
|
if (this.state.preferredRestrictionVersion) {
|
||||||
|
upgradeRequiredPill = <span className="mx_SecurityRoomSettingsTab_upgradeRequired">
|
||||||
|
{ _t("Upgrade required") }
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let description;
|
||||||
|
if (joinRule === JoinRule.Restricted && this.state.restrictedAllowRoomIds?.length) {
|
||||||
|
const shownSpaces = this.state.restrictedAllowRoomIds
|
||||||
|
.map(roomId => client.getRoom(roomId))
|
||||||
|
.filter(room => room?.isSpaceRoom())
|
||||||
|
.slice(0, 4);
|
||||||
|
|
||||||
|
let moreText;
|
||||||
|
if (shownSpaces.length < this.state.restrictedAllowRoomIds.length) {
|
||||||
|
if (shownSpaces.length > 0) {
|
||||||
|
moreText = _t("& %(count)s more", {
|
||||||
|
count: this.state.restrictedAllowRoomIds.length - shownSpaces.length,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
moreText = _t("Currently, %(count)s spaces have access", {
|
||||||
|
count: this.state.restrictedAllowRoomIds.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
description = <div>
|
||||||
|
<span>
|
||||||
|
{ _t("Anyone in a space can find and join. <a>Edit which spaces can access here.</a>", {}, {
|
||||||
|
a: sub => <AccessibleButton
|
||||||
|
disabled={!canChangeJoinRule}
|
||||||
|
onClick={this.onEditRestrictedClick}
|
||||||
|
kind="link"
|
||||||
|
>
|
||||||
|
{ sub }
|
||||||
|
</AccessibleButton>,
|
||||||
|
}) }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="mx_SecurityRoomSettingsTab_spacesWithAccess">
|
||||||
|
<h4>{ _t("Spaces with access") }</h4>
|
||||||
|
{ shownSpaces.map(room => {
|
||||||
|
return <span key={room.roomId}>
|
||||||
|
<RoomAvatar room={room} height={32} width={32} />
|
||||||
|
{ room.name }
|
||||||
|
</span>;
|
||||||
|
}) }
|
||||||
|
{ moreText && <span>{ moreText }</span> }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else if (SpaceStore.instance.activeSpace) {
|
||||||
|
description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", {
|
||||||
|
spaceName: SpaceStore.instance.activeSpace.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
description = _t("Anyone in a space can find and join. You can select multiple spaces.");
|
||||||
|
}
|
||||||
|
|
||||||
|
radioDefinitions.splice(1, 0, {
|
||||||
|
value: JoinRule.Restricted,
|
||||||
|
label: <>
|
||||||
|
{ _t("Space members") }
|
||||||
|
{ upgradeRequiredPill }
|
||||||
|
</>,
|
||||||
|
description,
|
||||||
|
// if there are 0 allowed spaces then render it as invite only instead
|
||||||
|
checked: this.state.joinRule === JoinRule.Restricted && !!this.state.restrictedAllowRoomIds?.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="mx_SecurityRoomSettingsTab_joinRule">
|
||||||
{ guestWarning }
|
<div className="mx_SettingsTab_subsectionText">
|
||||||
|
<span>{ _t("Decide who can join %(roomName)s.", {
|
||||||
|
roomName: client.getRoom(this.props.roomId)?.name,
|
||||||
|
}) }</span>
|
||||||
|
</div>
|
||||||
{ aliasWarning }
|
{ aliasWarning }
|
||||||
<StyledRadioGroup
|
<StyledRadioGroup
|
||||||
name="roomVis"
|
name="joinRule"
|
||||||
value={joinRule}
|
value={joinRule}
|
||||||
onChange={this.onRoomAccessRadioToggle}
|
onChange={this.onJoinRuleChange}
|
||||||
definitions={[
|
definitions={radioDefinitions}
|
||||||
{
|
disabled={!canChangeJoinRule}
|
||||||
value: "invite_only",
|
|
||||||
disabled: !canChangeAccess,
|
|
||||||
label: _t('Only people who have been invited'),
|
|
||||||
checked: joinRule !== "public",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "public_no_guests",
|
|
||||||
disabled: !canChangeAccess,
|
|
||||||
label: _t('Anyone who knows the room\'s link, apart from guests'),
|
|
||||||
checked: joinRule === "public" && guestAccess !== "can_join",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "public_with_guests",
|
|
||||||
disabled: !canChangeAccess,
|
|
||||||
label: _t("Anyone who knows the room's link, including guests"),
|
|
||||||
checked: joinRule === "public" && guestAccess === "can_join",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -347,7 +441,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const history = this.state.history;
|
const history = this.state.history;
|
||||||
const state = client.getRoom(this.props.roomId).currentState;
|
const state = client.getRoom(this.props.roomId).currentState;
|
||||||
const canChangeHistory = state.mayClientSendStateEvent('m.room.history_visibility', client);
|
const canChangeHistory = state.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client);
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
|
@ -389,11 +483,35 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private toggleAdvancedSection = () => {
|
||||||
|
this.setState({ showAdvancedSection: !this.state.showAdvancedSection });
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderAdvanced() {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const guestAccess = this.state.guestAccess;
|
||||||
|
const state = client.getRoom(this.props.roomId).currentState;
|
||||||
|
const canSetGuestAccess = state.mayClientSendStateEvent(EventType.RoomGuestAccess, client);
|
||||||
|
|
||||||
|
return <div className="mx_SettingsTab_section">
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={guestAccess === GuestAccess.CanJoin}
|
||||||
|
onChange={this.onGuestAccessChange}
|
||||||
|
disabled={!canSetGuestAccess}
|
||||||
|
label={_t("Enable guest access")}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
{ _t("People with supported clients will be able to join " +
|
||||||
|
"the room without having a registered account.") }
|
||||||
|
</p>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const isEncrypted = this.state.encrypted;
|
const isEncrypted = this.state.encrypted;
|
||||||
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent("m.room.encryption", client);
|
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client);
|
||||||
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
|
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
|
||||||
|
|
||||||
let encryptionSettings = null;
|
let encryptionSettings = null;
|
||||||
|
@ -436,11 +554,20 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
{ encryptionSettings }
|
{ encryptionSettings }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t("Who can access this room?") }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Access") }</span>
|
||||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||||
{ this.renderRoomAccess() }
|
{ this.renderJoinRule() }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={this.toggleAdvancedSection}
|
||||||
|
kind="link"
|
||||||
|
className="mx_SettingsTab_showAdvanced"
|
||||||
|
>
|
||||||
|
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
|
||||||
|
</AccessibleButton>
|
||||||
|
{ this.state.showAdvancedSection && this.renderAdvanced() }
|
||||||
|
|
||||||
{ historySection }
|
{ historySection }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,13 +18,13 @@ import React, { useState } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import AliasSettings from "../room_settings/AliasSettings";
|
import AliasSettings from "../room_settings/AliasSettings";
|
||||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
|
|
||||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
|
@ -203,7 +203,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
showSpaceSettings(this.context, this.props.space);
|
showSpaceSettings(this.props.space);
|
||||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -222,7 +222,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
showCreateNewRoom(this.context, this.props.space);
|
showCreateNewRoom(this.props.space);
|
||||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
showAddExistingRooms(this.context, this.props.space);
|
showAddExistingRooms(this.props.space);
|
||||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -285,7 +285,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
|
|
||||||
let settingsOption;
|
let settingsOption;
|
||||||
let leaveSection;
|
let leaveSection;
|
||||||
if (shouldShowSpaceSettings(this.context, this.props.space)) {
|
if (shouldShowSpaceSettings(this.props.space)) {
|
||||||
settingsOption = (
|
settingsOption = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_SpacePanel_iconSettings"
|
iconClassName="mx_SpacePanel_iconSettings"
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
import { JoinRule, Preset, RestrictedAllowType, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
@ -35,8 +37,6 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
|
||||||
import SpaceStore from "./stores/SpaceStore";
|
import SpaceStore from "./stores/SpaceStore";
|
||||||
import { makeSpaceParentEvent } from "./utils/space";
|
import { makeSpaceParentEvent } from "./utils/space";
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
|
||||||
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import Spinner from "./components/views/elements/Spinner";
|
import Spinner from "./components/views/elements/Spinner";
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ export interface IOpts {
|
||||||
andView?: boolean;
|
andView?: boolean;
|
||||||
associatedWithCommunity?: string;
|
associatedWithCommunity?: string;
|
||||||
parentSpace?: Room;
|
parentSpace?: Room;
|
||||||
|
joinRule?: JoinRule;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,7 +75,7 @@ export interface IOpts {
|
||||||
* @returns {Promise} which resolves to the room id, or null if the
|
* @returns {Promise} which resolves to the room id, or null if the
|
||||||
* action was aborted or failed.
|
* action was aborted or failed.
|
||||||
*/
|
*/
|
||||||
export default function createRoom(opts: IOpts): Promise<string | null> {
|
export default async function createRoom(opts: IOpts): Promise<string | null> {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
if (opts.spinner === undefined) opts.spinner = true;
|
if (opts.spinner === undefined) opts.spinner = true;
|
||||||
if (opts.guestAccess === undefined) opts.guestAccess = true;
|
if (opts.guestAccess === undefined) opts.guestAccess = true;
|
||||||
|
@ -85,7 +86,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (client.isGuest()) {
|
if (client.isGuest()) {
|
||||||
dis.dispatch({ action: 'require_registration' });
|
dis.dispatch({ action: 'require_registration' });
|
||||||
return Promise.resolve(null);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
|
const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
|
||||||
|
@ -142,13 +143,37 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.parentSpace) {
|
if (opts.parentSpace) {
|
||||||
opts.createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
|
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
|
||||||
opts.createOpts.initial_state.push({
|
createOpts.initial_state.push({
|
||||||
type: EventType.RoomHistoryVisibility,
|
type: EventType.RoomHistoryVisibility,
|
||||||
content: {
|
content: {
|
||||||
"history_visibility": opts.createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
|
"history_visibility": createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (opts.joinRule === JoinRule.Restricted) {
|
||||||
|
if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) {
|
||||||
|
createOpts.room_version = SpaceStore.instance.restrictedJoinRuleSupport.preferred;
|
||||||
|
|
||||||
|
createOpts.initial_state.push({
|
||||||
|
type: EventType.RoomJoinRules,
|
||||||
|
content: {
|
||||||
|
"join_rule": JoinRule.Restricted,
|
||||||
|
"allow": [{
|
||||||
|
"type": RestrictedAllowType.RoomMembership,
|
||||||
|
"room_id": opts.parentSpace.roomId,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.joinRule !== JoinRule.Restricted) {
|
||||||
|
createOpts.initial_state.push({
|
||||||
|
type: EventType.RoomJoinRules,
|
||||||
|
content: { join_rule: opts.joinRule },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal;
|
let modal;
|
||||||
|
|
|
@ -437,8 +437,6 @@
|
||||||
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
|
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
|
||||||
"Upgrades a room to a new version": "Upgrades a room to a new version",
|
"Upgrades a room to a new version": "Upgrades a room to a new version",
|
||||||
"You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.",
|
"You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.",
|
||||||
"Error upgrading room": "Error upgrading room",
|
|
||||||
"Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
|
|
||||||
"Changes your display nickname": "Changes your display nickname",
|
"Changes your display nickname": "Changes your display nickname",
|
||||||
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
|
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
|
||||||
"Changes the avatar of the current room": "Changes the avatar of the current room",
|
"Changes the avatar of the current room": "Changes the avatar of the current room",
|
||||||
|
@ -719,6 +717,8 @@
|
||||||
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
|
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
|
||||||
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
|
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
|
||||||
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
|
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
|
||||||
|
"Error upgrading room": "Error upgrading room",
|
||||||
|
"Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
|
||||||
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
|
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
|
||||||
"Share your public space": "Share your public space",
|
"Share your public space": "Share your public space",
|
||||||
"Unknown App": "Unknown App",
|
"Unknown App": "Unknown App",
|
||||||
|
@ -765,6 +765,16 @@
|
||||||
"The person who invited you already left the room.": "The person who invited you already left the room.",
|
"The person who invited you already left the room.": "The person who invited you already left the room.",
|
||||||
"The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.",
|
"The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.",
|
||||||
"Failed to join room": "Failed to join room",
|
"Failed to join room": "Failed to join room",
|
||||||
|
"New in the Spaces beta": "New in the Spaces beta",
|
||||||
|
"Help people in spaces to find and join private rooms": "Help people in spaces to find and join private rooms",
|
||||||
|
"Learn more": "Learn more",
|
||||||
|
"Help space members find private rooms": "Help space members find private rooms",
|
||||||
|
"To help space members find and join a private room, go to that room's Security & Privacy settings.": "To help space members find and join a private room, go to that room's Security & Privacy settings.",
|
||||||
|
"General": "General",
|
||||||
|
"Security & Privacy": "Security & Privacy",
|
||||||
|
"Roles & Permissions": "Roles & Permissions",
|
||||||
|
"This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.",
|
||||||
|
"Skip": "Skip",
|
||||||
"You joined the call": "You joined the call",
|
"You joined the call": "You joined the call",
|
||||||
"%(senderName)s joined the call": "%(senderName)s joined the call",
|
"%(senderName)s joined the call": "%(senderName)s joined the call",
|
||||||
"Call in progress": "Call in progress",
|
"Call in progress": "Call in progress",
|
||||||
|
@ -1025,7 +1035,6 @@
|
||||||
"Invite people": "Invite people",
|
"Invite people": "Invite people",
|
||||||
"Invite with email or username": "Invite with email or username",
|
"Invite with email or username": "Invite with email or username",
|
||||||
"Failed to save space settings.": "Failed to save space settings.",
|
"Failed to save space settings.": "Failed to save space settings.",
|
||||||
"General": "General",
|
|
||||||
"Edit settings relating to your space.": "Edit settings relating to your space.",
|
"Edit settings relating to your space.": "Edit settings relating to your space.",
|
||||||
"Saving...": "Saving...",
|
"Saving...": "Saving...",
|
||||||
"Save Changes": "Save Changes",
|
"Save Changes": "Save Changes",
|
||||||
|
@ -1415,27 +1424,34 @@
|
||||||
"Muted Users": "Muted Users",
|
"Muted Users": "Muted Users",
|
||||||
"Banned users": "Banned users",
|
"Banned users": "Banned users",
|
||||||
"Send %(eventType)s events": "Send %(eventType)s events",
|
"Send %(eventType)s events": "Send %(eventType)s events",
|
||||||
"Roles & Permissions": "Roles & Permissions",
|
|
||||||
"Permissions": "Permissions",
|
"Permissions": "Permissions",
|
||||||
"Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room",
|
"Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room",
|
||||||
"Enable encryption?": "Enable encryption?",
|
"Enable encryption?": "Enable encryption?",
|
||||||
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
|
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
|
||||||
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
"This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
|
||||||
"Click here to fix": "Click here to fix",
|
|
||||||
"To link to this room, please add an address.": "To link to this room, please add an address.",
|
"To link to this room, please add an address.": "To link to this room, please add an address.",
|
||||||
"Only people who have been invited": "Only people who have been invited",
|
"Private (invite only)": "Private (invite only)",
|
||||||
"Anyone who knows the room's link, apart from guests": "Anyone who knows the room's link, apart from guests",
|
"Only invited people can join.": "Only invited people can join.",
|
||||||
"Anyone who knows the room's link, including guests": "Anyone who knows the room's link, including guests",
|
"Anyone can find and join.": "Anyone can find and join.",
|
||||||
|
"Upgrade required": "Upgrade required",
|
||||||
|
"& %(count)s more|other": "& %(count)s more",
|
||||||
|
"Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access",
|
||||||
|
"Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Anyone in a space can find and join. <a>Edit which spaces can access here.</a>",
|
||||||
|
"Spaces with access": "Spaces with access",
|
||||||
|
"Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Anyone in %(spaceName)s can find and join. You can select other spaces too.",
|
||||||
|
"Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.",
|
||||||
|
"Space members": "Space members",
|
||||||
|
"Decide who can join %(roomName)s.": "Decide who can join %(roomName)s.",
|
||||||
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
|
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
|
||||||
"Members only (since they were invited)": "Members only (since they were invited)",
|
"Members only (since they were invited)": "Members only (since they were invited)",
|
||||||
"Members only (since they joined)": "Members only (since they joined)",
|
"Members only (since they joined)": "Members only (since they joined)",
|
||||||
"Anyone": "Anyone",
|
"Anyone": "Anyone",
|
||||||
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.",
|
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.",
|
||||||
|
"People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.",
|
||||||
"Who can read history?": "Who can read history?",
|
"Who can read history?": "Who can read history?",
|
||||||
"Security & Privacy": "Security & Privacy",
|
|
||||||
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
||||||
"Encrypted": "Encrypted",
|
"Encrypted": "Encrypted",
|
||||||
"Who can access this room?": "Who can access this room?",
|
"Access": "Access",
|
||||||
"Unable to revoke sharing for email address": "Unable to revoke sharing for email address",
|
"Unable to revoke sharing for email address": "Unable to revoke sharing for email address",
|
||||||
"Unable to share email address": "Unable to share email address",
|
"Unable to share email address": "Unable to share email address",
|
||||||
"Your email address hasn't been verified yet": "Your email address hasn't been verified yet",
|
"Your email address hasn't been verified yet": "Your email address hasn't been verified yet",
|
||||||
|
@ -2147,7 +2163,6 @@
|
||||||
"People you know on %(brand)s": "People you know on %(brand)s",
|
"People you know on %(brand)s": "People you know on %(brand)s",
|
||||||
"Hide": "Hide",
|
"Hide": "Hide",
|
||||||
"Show": "Show",
|
"Show": "Show",
|
||||||
"Skip": "Skip",
|
|
||||||
"Send %(count)s invites|other": "Send %(count)s invites",
|
"Send %(count)s invites|other": "Send %(count)s invites",
|
||||||
"Send %(count)s invites|one": "Send %(count)s invite",
|
"Send %(count)s invites|one": "Send %(count)s invite",
|
||||||
"Invite people to join %(communityName)s": "Invite people to join %(communityName)s",
|
"Invite people to join %(communityName)s": "Invite people to join %(communityName)s",
|
||||||
|
@ -2176,18 +2191,25 @@
|
||||||
"Community ID": "Community ID",
|
"Community ID": "Community ID",
|
||||||
"example": "example",
|
"example": "example",
|
||||||
"Please enter a name for the room": "Please enter a name for the room",
|
"Please enter a name for the room": "Please enter a name for the room",
|
||||||
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.",
|
|
||||||
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.",
|
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.",
|
||||||
|
"Everyone in <SpaceName/> will be able to find and join this room.": "Everyone in <SpaceName/> will be able to find and join this room.",
|
||||||
|
"You can change this at any time from room settings.": "You can change this at any time from room settings.",
|
||||||
|
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Anyone will be able to find and join this room, not just members of <SpaceName/>.",
|
||||||
|
"Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.",
|
||||||
"You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.",
|
"You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.",
|
||||||
"Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.",
|
"Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.",
|
||||||
"Enable end-to-end encryption": "Enable end-to-end encryption",
|
"Enable end-to-end encryption": "Enable end-to-end encryption",
|
||||||
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.",
|
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.",
|
||||||
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.",
|
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.",
|
||||||
|
"Create a room": "Create a room",
|
||||||
|
"Create a room in %(communityName)s": "Create a room in %(communityName)s",
|
||||||
"Create a public room": "Create a public room",
|
"Create a public room": "Create a public room",
|
||||||
"Create a private room": "Create a private room",
|
"Create a private room": "Create a private room",
|
||||||
"Create a room in %(communityName)s": "Create a room in %(communityName)s",
|
"Private room (invite only)": "Private room (invite only)",
|
||||||
|
"Public room": "Public room",
|
||||||
|
"Visible to space members": "Visible to space members",
|
||||||
"Topic (optional)": "Topic (optional)",
|
"Topic (optional)": "Topic (optional)",
|
||||||
"Make this room public": "Make this room public",
|
"Room visibility": "Room visibility",
|
||||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
||||||
"Create Room": "Create Room",
|
"Create Room": "Create Room",
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
|
@ -2341,6 +2363,17 @@
|
||||||
"Manually export keys": "Manually export keys",
|
"Manually export keys": "Manually export keys",
|
||||||
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
|
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
|
||||||
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
|
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
|
||||||
|
"%(count)s members|other": "%(count)s members",
|
||||||
|
"%(count)s members|one": "%(count)s member",
|
||||||
|
"%(count)s rooms|other": "%(count)s rooms",
|
||||||
|
"%(count)s rooms|one": "%(count)s room",
|
||||||
|
"You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only",
|
||||||
|
"Select spaces": "Select spaces",
|
||||||
|
"Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.": "Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.",
|
||||||
|
"Search spaces": "Search spaces",
|
||||||
|
"Spaces you know that contain this room": "Spaces you know that contain this room",
|
||||||
|
"Other spaces or rooms you might not know": "Other spaces or rooms you might not know",
|
||||||
|
"These are likely ones other room admins are a part of.": "These are likely ones other room admins are a part of.",
|
||||||
"Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:",
|
"Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:",
|
||||||
"Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:",
|
"Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:",
|
||||||
"Session name": "Session name",
|
"Session name": "Session name",
|
||||||
|
@ -2384,12 +2417,13 @@
|
||||||
"Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room",
|
"Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room",
|
||||||
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room",
|
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room",
|
||||||
"Put a link back to the old room at the start of the new room so people can see old messages": "Put a link back to the old room at the start of the new room so people can see old messages",
|
"Put a link back to the old room at the start of the new room so people can see old messages": "Put a link back to the old room at the start of the new room so people can see old messages",
|
||||||
"Automatically invite users": "Automatically invite users",
|
"Automatically invite members from this room to the new one": "Automatically invite members from this room to the new one",
|
||||||
"Upgrade private room": "Upgrade private room",
|
"Upgrade private room": "Upgrade private room",
|
||||||
"Upgrade public room": "Upgrade public room",
|
"Upgrade public room": "Upgrade public room",
|
||||||
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.",
|
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.",
|
||||||
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.",
|
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.",
|
||||||
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
|
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
|
||||||
|
"<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.": "<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.",
|
||||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||||
"Resend": "Resend",
|
"Resend": "Resend",
|
||||||
"You're all caught up.": "You're all caught up.",
|
"You're all caught up.": "You're all caught up.",
|
||||||
|
@ -2412,7 +2446,6 @@
|
||||||
"We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.",
|
"We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.",
|
||||||
"Other homeserver": "Other homeserver",
|
"Other homeserver": "Other homeserver",
|
||||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
|
"Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
|
||||||
"Learn more": "Learn more",
|
|
||||||
"About homeservers": "About homeservers",
|
"About homeservers": "About homeservers",
|
||||||
"Reset event store?": "Reset event store?",
|
"Reset event store?": "Reset event store?",
|
||||||
"You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store",
|
"You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store",
|
||||||
|
@ -2647,6 +2680,7 @@
|
||||||
"You are an administrator of this community": "You are an administrator of this community",
|
"You are an administrator of this community": "You are an administrator of this community",
|
||||||
"You are a member of this community": "You are a member of this community",
|
"You are a member of this community": "You are a member of this community",
|
||||||
"Who can join this community?": "Who can join this community?",
|
"Who can join this community?": "Who can join this community?",
|
||||||
|
"Only people who have been invited": "Only people who have been invited",
|
||||||
"Everyone": "Everyone",
|
"Everyone": "Everyone",
|
||||||
"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!",
|
"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)",
|
"Long Description (HTML)": "Long Description (HTML)",
|
||||||
|
@ -2746,10 +2780,6 @@
|
||||||
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
|
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
|
||||||
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
|
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
|
||||||
"You don't have permission": "You don't have permission",
|
"You don't have permission": "You don't have permission",
|
||||||
"%(count)s members|other": "%(count)s members",
|
|
||||||
"%(count)s members|one": "%(count)s member",
|
|
||||||
"%(count)s rooms|other": "%(count)s rooms",
|
|
||||||
"%(count)s rooms|one": "%(count)s room",
|
|
||||||
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
|
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
|
||||||
"Suggested": "Suggested",
|
"Suggested": "Suggested",
|
||||||
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
|
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
|
||||||
|
|
|
@ -14,11 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { ListIteratee, Many, sortBy, throttle } from "lodash";
|
import { ListIteratee, Many, sortBy, throttle } from "lodash";
|
||||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { ISpaceSummaryRoom } from "matrix-js-sdk/src/@types/spaces";
|
import { ISpaceSummaryRoom } from "matrix-js-sdk/src/@types/spaces";
|
||||||
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { IRoomCapability } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
|
@ -39,6 +42,12 @@ import { objectDiff } from "../utils/objects";
|
||||||
import { arrayHasOrderChange } from "../utils/arrays";
|
import { arrayHasOrderChange } from "../utils/arrays";
|
||||||
import { reorderLexicographically } from "../utils/stringOrderField";
|
import { reorderLexicographically } from "../utils/stringOrderField";
|
||||||
import { TAG_ORDER } from "../components/views/rooms/RoomList";
|
import { TAG_ORDER } from "../components/views/rooms/RoomList";
|
||||||
|
import { shouldShowSpaceSettings } from "../utils/space";
|
||||||
|
import ToastStore from "./ToastStore";
|
||||||
|
import { _t } from "../languageHandler";
|
||||||
|
import GenericToast from "../components/views/toasts/GenericToast";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import InfoDialog from "../components/views/dialogs/InfoDialog";
|
||||||
|
|
||||||
type SpaceKey = string | symbol;
|
type SpaceKey = string | symbol;
|
||||||
|
|
||||||
|
@ -114,6 +123,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private _suggestedRooms: ISuggestedRoom[] = [];
|
private _suggestedRooms: ISuggestedRoom[] = [];
|
||||||
private _invitedSpaces = new Set<Room>();
|
private _invitedSpaces = new Set<Room>();
|
||||||
private spaceOrderLocalEchoMap = new Map<string, string>();
|
private spaceOrderLocalEchoMap = new Map<string, string>();
|
||||||
|
private _restrictedJoinRuleSupport?: IRoomCapability;
|
||||||
|
|
||||||
public get invitedSpaces(): Room[] {
|
public get invitedSpaces(): Room[] {
|
||||||
return Array.from(this._invitedSpaces);
|
return Array.from(this._invitedSpaces);
|
||||||
|
@ -131,7 +141,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return this._suggestedRooms;
|
return this._suggestedRooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setActiveRoomInSpace(space: Room | null) {
|
public async setActiveRoomInSpace(space: Room | null): Promise<void> {
|
||||||
if (space && !space.isSpaceRoom()) return;
|
if (space && !space.isSpaceRoom()) return;
|
||||||
if (space !== this.activeSpace) await this.setActiveSpace(space);
|
if (space !== this.activeSpace) await this.setActiveSpace(space);
|
||||||
|
|
||||||
|
@ -166,6 +176,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get restrictedJoinRuleSupport(): IRoomCapability {
|
||||||
|
return this._restrictedJoinRuleSupport;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the active space, updates room list filters,
|
* Sets the active space, updates room list filters,
|
||||||
* optionally switches the user's room back to where they were when they last viewed that space.
|
* optionally switches the user's room back to where they were when they last viewed that space.
|
||||||
|
@ -215,6 +229,65 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
|
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New in Spaces beta toast for Restricted Join Rule
|
||||||
|
const lsKey = "mx_SpaceBeta_restrictedJoinRuleToastSeen";
|
||||||
|
if (contextSwitch && space?.getJoinRule() === JoinRule.Invite && shouldShowSpaceSettings(space) &&
|
||||||
|
space.getJoinedMemberCount() > 1 && !localStorage.getItem(lsKey)
|
||||||
|
&& this.restrictedJoinRuleSupport?.preferred
|
||||||
|
) {
|
||||||
|
const toastKey = "restrictedjoinrule";
|
||||||
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
key: toastKey,
|
||||||
|
title: _t("New in the Spaces beta"),
|
||||||
|
props: {
|
||||||
|
description: _t("Help people in spaces to find and join private rooms"),
|
||||||
|
acceptLabel: _t("Learn more"),
|
||||||
|
onAccept: () => {
|
||||||
|
localStorage.setItem(lsKey, "true");
|
||||||
|
ToastStore.sharedInstance().dismissToast(toastKey);
|
||||||
|
|
||||||
|
Modal.createTrackedDialog("New in the Spaces beta", "restricted join rule", InfoDialog, {
|
||||||
|
title: _t("Help space members find private rooms"),
|
||||||
|
description: <>
|
||||||
|
<p>{ _t("To help space members find and join a private room, " +
|
||||||
|
"go to that room's Security & Privacy settings.") }</p>
|
||||||
|
|
||||||
|
{ /* Reuses classes from TabbedView for simplicity, non-interactive */ }
|
||||||
|
<div style={{ width: "190px" }}>
|
||||||
|
<div className="mx_TabbedView_tabLabel">
|
||||||
|
<span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon" />
|
||||||
|
<span className="mx_TabbedView_tabLabel_text">{ _t("General") }</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active">
|
||||||
|
<span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_securityIcon" />
|
||||||
|
<span className="mx_TabbedView_tabLabel_text">{ _t("Security & Privacy") }</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx_TabbedView_tabLabel">
|
||||||
|
<span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_rolesIcon" />
|
||||||
|
<span className="mx_TabbedView_tabLabel_text">{ _t("Roles & Permissions") }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{ _t("This makes it easy for rooms to stay private to a space, " +
|
||||||
|
"while letting people in the space find and join them. " +
|
||||||
|
"All new rooms in a space will have this option available.") }</p>
|
||||||
|
</>,
|
||||||
|
button: _t("OK"),
|
||||||
|
hasCloseButton: false,
|
||||||
|
fixedWidth: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rejectLabel: _t("Skip"),
|
||||||
|
onReject: () => {
|
||||||
|
localStorage.setItem(lsKey, "true");
|
||||||
|
ToastStore.sharedInstance().dismissToast(toastKey);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
component: GenericToast,
|
||||||
|
priority: 35,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (space) {
|
if (space) {
|
||||||
const suggestedRooms = await this.fetchSuggestedRooms(space);
|
const suggestedRooms = await this.fetchSuggestedRooms(space);
|
||||||
if (this._activeSpace === space) {
|
if (this._activeSpace === space) {
|
||||||
|
@ -301,6 +374,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return sortBy(parents, r => r.roomId)?.[0] || null;
|
return sortBy(parents, r => r.roomId)?.[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getKnownParents(roomId: string): Set<string> {
|
||||||
|
return this.parentMap.get(roomId) || new Set();
|
||||||
|
}
|
||||||
|
|
||||||
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
||||||
if (!space && spacesTweakAllRoomsEnabled) {
|
if (!space && spacesTweakAllRoomsEnabled) {
|
||||||
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
||||||
|
@ -678,6 +755,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.matrixClient.on("accountData", this.onAccountData);
|
this.matrixClient.on("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.matrixClient.getCapabilities().then(capabilities => {
|
||||||
|
this._restrictedJoinRuleSupport = capabilities
|
||||||
|
?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"];
|
||||||
|
});
|
||||||
|
|
||||||
await this.onSpaceUpdate(); // trigger an initial update
|
await this.onSpaceUpdate(); // trigger an initial update
|
||||||
|
|
||||||
// restore selected state from last session if any and still valid
|
// restore selected state from last session if any and still valid
|
||||||
|
|
|
@ -67,6 +67,7 @@ export class MediaEventHelper implements IDestroyable {
|
||||||
private prepareThumbnailUrl = async () => {
|
private prepareThumbnailUrl = async () => {
|
||||||
if (this.media.isEncrypted) {
|
if (this.media.isEncrypted) {
|
||||||
const blob = await this.thumbnailBlob.value;
|
const blob = await this.thumbnailBlob.value;
|
||||||
|
if (blob === null) return null;
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
} else {
|
} else {
|
||||||
return this.media.thumbnailHttp;
|
return this.media.thumbnailHttp;
|
||||||
|
|
93
src/utils/RoomUpgrade.ts
Normal file
93
src/utils/RoomUpgrade.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
import { inviteUsersToRoom } from "../RoomInvite";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import { _t } from "../languageHandler";
|
||||||
|
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||||
|
import SpaceStore from "../stores/SpaceStore";
|
||||||
|
|
||||||
|
export async function upgradeRoom(
|
||||||
|
room: Room,
|
||||||
|
targetVersion: string,
|
||||||
|
inviteUsers = false,
|
||||||
|
handleError = true,
|
||||||
|
updateSpaces = true,
|
||||||
|
): Promise<string> {
|
||||||
|
const cli = room.client;
|
||||||
|
|
||||||
|
let newRoomId: string;
|
||||||
|
try {
|
||||||
|
({ replacement_room: newRoomId } = await cli.upgradeRoom(room.roomId, targetVersion));
|
||||||
|
} catch (e) {
|
||||||
|
if (!handleError) throw e;
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
Modal.createTrackedDialog("Room Upgrade Error", "", ErrorDialog, {
|
||||||
|
title: _t('Error upgrading room'),
|
||||||
|
description: _t('Double check that your server supports the room version chosen and try again.'),
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to wait for the js-sdk to give us the room back so
|
||||||
|
// we can more effectively abuse the MultiInviter behaviour
|
||||||
|
// which heavily relies on the Room object being available.
|
||||||
|
if (inviteUsers) {
|
||||||
|
const checkForUpgradeFn = async (newRoom: Room): Promise<void> => {
|
||||||
|
// The upgradePromise should be done by the time we await it here.
|
||||||
|
if (newRoom.roomId !== newRoomId) return;
|
||||||
|
|
||||||
|
const toInvite = [
|
||||||
|
...room.getMembersWithMembership("join"),
|
||||||
|
...room.getMembersWithMembership("invite"),
|
||||||
|
].map(m => m.userId).filter(m => m !== cli.getUserId());
|
||||||
|
|
||||||
|
if (toInvite.length > 0) {
|
||||||
|
// Errors are handled internally to this function
|
||||||
|
await inviteUsersToRoom(newRoomId, toInvite);
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.removeListener('Room', checkForUpgradeFn);
|
||||||
|
};
|
||||||
|
cli.on('Room', checkForUpgradeFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateSpaces) {
|
||||||
|
const parents = SpaceStore.instance.getKnownParents(room.roomId);
|
||||||
|
try {
|
||||||
|
for (const parentId of parents) {
|
||||||
|
const parent = cli.getRoom(parentId);
|
||||||
|
if (!parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())) continue;
|
||||||
|
|
||||||
|
const currentEv = parent.currentState.getStateEvents(EventType.SpaceChild, room.roomId);
|
||||||
|
await cli.sendStateEvent(parentId, EventType.SpaceChild, {
|
||||||
|
...(currentEv?.getContent() || {}), // copy existing attributes like suggested
|
||||||
|
via: [cli.getDomain()],
|
||||||
|
}, newRoomId);
|
||||||
|
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, room.roomId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// These errors are not critical to the room upgrade itself
|
||||||
|
console.warn("Failed to update parent spaces during room upgrade", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRoomId;
|
||||||
|
}
|
|
@ -16,10 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import { calculateRoomVia } from "../utils/permalinks/Permalinks";
|
import { calculateRoomVia } from "./permalinks/Permalinks";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
|
import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
|
||||||
import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog";
|
import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog";
|
||||||
|
@ -30,8 +29,8 @@ import SpacePublicShare from "../components/views/spaces/SpacePublicShare";
|
||||||
import InfoDialog from "../components/views/dialogs/InfoDialog";
|
import InfoDialog from "../components/views/dialogs/InfoDialog";
|
||||||
import { showRoomInviteDialog } from "../RoomInvite";
|
import { showRoomInviteDialog } from "../RoomInvite";
|
||||||
|
|
||||||
export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
|
export const shouldShowSpaceSettings = (space: Room) => {
|
||||||
const userId = cli.getUserId();
|
const userId = space.client.getUserId();
|
||||||
return space.getMyMembership() === "join"
|
return space.getMyMembership() === "join"
|
||||||
&& (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
|
&& (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
|
||||||
|| space.currentState.maySendStateEvent(EventType.RoomName, userId)
|
|| space.currentState.maySendStateEvent(EventType.RoomName, userId)
|
||||||
|
@ -48,20 +47,20 @@ export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
|
||||||
state_key: room.roomId,
|
state_key: room.roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const showSpaceSettings = (cli: MatrixClient, space: Room) => {
|
export const showSpaceSettings = (space: Room) => {
|
||||||
Modal.createTrackedDialog("Space Settings", "", SpaceSettingsDialog, {
|
Modal.createTrackedDialog("Space Settings", "", SpaceSettingsDialog, {
|
||||||
matrixClient: cli,
|
matrixClient: space.client,
|
||||||
space,
|
space,
|
||||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showAddExistingRooms = async (cli: MatrixClient, space: Room) => {
|
export const showAddExistingRooms = async (space: Room) => {
|
||||||
return Modal.createTrackedDialog(
|
return Modal.createTrackedDialog(
|
||||||
"Space Landing",
|
"Space Landing",
|
||||||
"Add Existing",
|
"Add Existing",
|
||||||
AddExistingToSpaceDialog,
|
AddExistingToSpaceDialog,
|
||||||
{
|
{
|
||||||
matrixClient: cli,
|
matrixClient: space.client,
|
||||||
onCreateRoomClick: showCreateNewRoom,
|
onCreateRoomClick: showCreateNewRoom,
|
||||||
space,
|
space,
|
||||||
},
|
},
|
||||||
|
@ -69,7 +68,7 @@ export const showAddExistingRooms = async (cli: MatrixClient, space: Room) => {
|
||||||
).finished;
|
).finished;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
|
export const showCreateNewRoom = async (space: Room) => {
|
||||||
const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
|
const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
|
||||||
"Space Landing",
|
"Space Landing",
|
||||||
"Create Room",
|
"Create Room",
|
||||||
|
|
|
@ -25,7 +25,7 @@ module.exports = async function roomDirectoryScenarios(alice, bob) {
|
||||||
console.log(" creating a public room and join through directory:");
|
console.log(" creating a public room and join through directory:");
|
||||||
const room = 'test';
|
const room = 'test';
|
||||||
await createRoom(alice, room);
|
await createRoom(alice, room);
|
||||||
await changeRoomSettings(alice, { directory: true, visibility: "public_no_guests", alias: "#test" });
|
await changeRoomSettings(alice, { directory: true, visibility: "public", alias: "#test" });
|
||||||
await join(bob, room); //looks up room in directory
|
await join(bob, room); //looks up room in directory
|
||||||
const bobMessage = "hi Alice!";
|
const bobMessage = "hi Alice!";
|
||||||
await sendMessage(bob, bobMessage);
|
await sendMessage(bob, bobMessage);
|
||||||
|
|
|
@ -51,7 +51,7 @@ const charlyMsg2 = "how's it going??";
|
||||||
|
|
||||||
async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) {
|
async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) {
|
||||||
await createRoom(bob, room);
|
await createRoom(bob, room);
|
||||||
await changeRoomSettings(bob, { directory: true, visibility: "public_no_guests", alias });
|
await changeRoomSettings(bob, { directory: true, visibility: "public", alias });
|
||||||
// wait for alias to be set by server after clicking "save"
|
// wait for alias to be set by server after clicking "save"
|
||||||
// so the charlies can join it.
|
// so the charlies can join it.
|
||||||
await bob.delay(500);
|
await bob.delay(500);
|
||||||
|
|
|
@ -98,18 +98,14 @@ async function checkRoomSettings(session, expectedSettings) {
|
||||||
if (expectedSettings.visibility) {
|
if (expectedSettings.visibility) {
|
||||||
session.log.step(`checks visibility is ${expectedSettings.visibility}`);
|
session.log.step(`checks visibility is ${expectedSettings.visibility}`);
|
||||||
const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]");
|
const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]");
|
||||||
assert.equal(radios.length, 7);
|
assert.equal(radios.length, 6);
|
||||||
const inviteOnly = radios[0];
|
const [inviteOnlyRoom, publicRoom] = radios;
|
||||||
const publicNoGuests = radios[1];
|
|
||||||
const publicWithGuests = radios[2];
|
|
||||||
|
|
||||||
let expectedRadio = null;
|
let expectedRadio = null;
|
||||||
if (expectedSettings.visibility === "invite_only") {
|
if (expectedSettings.visibility === "invite_only") {
|
||||||
expectedRadio = inviteOnly;
|
expectedRadio = inviteOnlyRoom;
|
||||||
} else if (expectedSettings.visibility === "public_no_guests") {
|
} else if (expectedSettings.visibility === "public") {
|
||||||
expectedRadio = publicNoGuests;
|
expectedRadio = publicRoom;
|
||||||
} else if (expectedSettings.visibility === "public_with_guests") {
|
|
||||||
expectedRadio = publicWithGuests;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`);
|
throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`);
|
||||||
}
|
}
|
||||||
|
@ -165,17 +161,13 @@ async function changeRoomSettings(session, settings) {
|
||||||
if (settings.visibility) {
|
if (settings.visibility) {
|
||||||
session.log.step(`sets visibility to ${settings.visibility}`);
|
session.log.step(`sets visibility to ${settings.visibility}`);
|
||||||
const radios = await session.queryAll(".mx_RoomSettingsDialog label");
|
const radios = await session.queryAll(".mx_RoomSettingsDialog label");
|
||||||
assert.equal(radios.length, 7);
|
assert.equal(radios.length, 6);
|
||||||
const inviteOnly = radios[0];
|
const [inviteOnlyRoom, publicRoom] = radios;
|
||||||
const publicNoGuests = radios[1];
|
|
||||||
const publicWithGuests = radios[2];
|
|
||||||
|
|
||||||
if (settings.visibility === "invite_only") {
|
if (settings.visibility === "invite_only") {
|
||||||
await inviteOnly.click();
|
await inviteOnlyRoom.click();
|
||||||
} else if (settings.visibility === "public_no_guests") {
|
} else if (settings.visibility === "public") {
|
||||||
await publicNoGuests.click();
|
await publicRoom.click();
|
||||||
} else if (settings.visibility === "public_with_guests") {
|
|
||||||
await publicWithGuests.click();
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`unrecognized room visibility setting: ${settings.visibility}`);
|
throw new Error(`unrecognized room visibility setting: ${settings.visibility}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,32 +52,6 @@ const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.
|
||||||
|
|
||||||
const testUserId = "@test:user";
|
const testUserId = "@test:user";
|
||||||
|
|
||||||
let rooms = [];
|
|
||||||
|
|
||||||
const mkRoom = (roomId: string) => {
|
|
||||||
const room = mkStubRoom(roomId);
|
|
||||||
room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([]));
|
|
||||||
rooms.push(room);
|
|
||||||
return room;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mkSpace = (spaceId: string, children: string[] = []) => {
|
|
||||||
const space = mkRoom(spaceId);
|
|
||||||
space.isSpaceRoom.mockReturnValue(true);
|
|
||||||
space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId =>
|
|
||||||
mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: EventType.SpaceChild,
|
|
||||||
room: spaceId,
|
|
||||||
user: testUserId,
|
|
||||||
skey: roomId,
|
|
||||||
content: { via: [] },
|
|
||||||
ts: Date.now(),
|
|
||||||
}),
|
|
||||||
)));
|
|
||||||
return space;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserIdForRoomId = jest.fn();
|
const getUserIdForRoomId = jest.fn();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
DMRoomMap.sharedInstance = { getUserIdForRoomId };
|
DMRoomMap.sharedInstance = { getUserIdForRoomId };
|
||||||
|
@ -107,6 +81,32 @@ describe("SpaceStore", () => {
|
||||||
const store = SpaceStore.instance;
|
const store = SpaceStore.instance;
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
let rooms = [];
|
||||||
|
|
||||||
|
const mkRoom = (roomId: string) => {
|
||||||
|
const room = mkStubRoom(roomId, roomId, client);
|
||||||
|
room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([]));
|
||||||
|
rooms.push(room);
|
||||||
|
return room;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mkSpace = (spaceId: string, children: string[] = []) => {
|
||||||
|
const space = mkRoom(spaceId);
|
||||||
|
space.isSpaceRoom.mockReturnValue(true);
|
||||||
|
space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId =>
|
||||||
|
mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.SpaceChild,
|
||||||
|
room: spaceId,
|
||||||
|
user: testUserId,
|
||||||
|
skey: roomId,
|
||||||
|
content: { via: [] },
|
||||||
|
ts: Date.now(),
|
||||||
|
}),
|
||||||
|
)));
|
||||||
|
return space;
|
||||||
|
};
|
||||||
|
|
||||||
const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true);
|
const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true);
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
|
|
|
@ -97,6 +97,7 @@ export function createTestClient() {
|
||||||
},
|
},
|
||||||
decryptEventIfNeeded: () => Promise.resolve(),
|
decryptEventIfNeeded: () => Promise.resolve(),
|
||||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||||
|
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,7 +221,7 @@ export function mkMessage(opts) {
|
||||||
return mkEvent(opts);
|
return mkEvent(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mkStubRoom(roomId = null, name) {
|
export function mkStubRoom(roomId = null, name, client) {
|
||||||
const stubTimeline = { getEvents: () => [] };
|
const stubTimeline = { getEvents: () => [] };
|
||||||
return {
|
return {
|
||||||
roomId,
|
roomId,
|
||||||
|
@ -235,6 +236,7 @@ export function mkStubRoom(roomId = null, name) {
|
||||||
}),
|
}),
|
||||||
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
||||||
getJoinedMembers: jest.fn().mockReturnValue([]),
|
getJoinedMembers: jest.fn().mockReturnValue([]),
|
||||||
|
getJoinedMemberCount: jest.fn().mockReturnValue(1),
|
||||||
getMembers: jest.fn().mockReturnValue([]),
|
getMembers: jest.fn().mockReturnValue([]),
|
||||||
getPendingEvents: () => [],
|
getPendingEvents: () => [],
|
||||||
getLiveTimeline: () => stubTimeline,
|
getLiveTimeline: () => stubTimeline,
|
||||||
|
@ -269,6 +271,8 @@ export function mkStubRoom(roomId = null, name) {
|
||||||
getCanonicalAlias: jest.fn(),
|
getCanonicalAlias: jest.fn(),
|
||||||
getAltAliases: jest.fn().mockReturnValue([]),
|
getAltAliases: jest.fn().mockReturnValue([]),
|
||||||
timeline: [],
|
timeline: [],
|
||||||
|
getJoinRule: jest.fn().mockReturnValue("invite"),
|
||||||
|
client,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue