Tweaks to informational architecture 1.1 (#7052)

* Move user avatar to Space panel

* Add room list header for 'Home' or 'Space Name' to room list
Add existing Space context menus to room list header

* Re-add pending room join spinner

* Iterate RoomListHeader plus context menu

* Iterate space context menu

* Iterate room list + interactions

* Move DND to new iA model

* Replace composer custom status management with usermenu one

* Cull Quick Actions

* Iterate minimized room list state

* delint

* Merge the RoomListNumResults into the RoomListHeader

* Make the search shortcut prompt semi-bold

* Iterate RoomListHeader based on design review

* Iterate UserMenu based on feedback

* Add name to expanded spacepanel usermenu button

* i18n

* Make room sub list aux button components more generic

* Change left panel explore button to only refer to room directory

* Iterate RoomListHeader

* Fix custom user status input field width in Chrome

* Bring back Notification settings button

* delint

* i18n

* post-merge fix

* iterate pr

* Remove unused state

* update copy

* Apply suggestions from PR review

* delint

* Update invite iconography

* Iterate Space context menu to match Figma

* Fix chevron alignment

* Fix edge case for RoomListHeader on metaspaces

* Wire up general rageshake-driven feedback mechanism

* Add IA1.1 info toast

* add missing alt attribute

* delint

* delint

* tweak ia toast priority

* e2e test account for new toast

* autofocus feedback field and remove old subheading

* tweak copy

* Iterate space panel colours to match Figma

* Iterate PR

* delint

* Fix feedback submission with object setting values

* iterate based on review

* Tweak colours and update splash image

* Tweaks based on review

* Remove room list prompt, made redundant by the big fat `+`

* Fix edge cases around User Menu positioning and dnd

* Add missing import, bad merge?

* Update aria label in e2e test

* Fix room list space rooms context menu explore button behaviour

* Tweak copy

* Revert order of options in the UserMenu

* Tweak copy

* i18n
This commit is contained in:
Michael Telatynski 2021-11-30 18:08:46 +00:00 committed by GitHub
parent c09e0efdb9
commit 8fe582b094
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1433 additions and 1483 deletions

View File

@ -482,6 +482,15 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
overflow-y: hidden;
}
.mx_DialogDesignChanges_wrapper .mx_Dialog_fixedWidth {
max-width: 636px; // match splash image width
.mx_AccessibleButton_kind_link {
font-size: inherit;
padding: 0;
}
}
// TODO: Review mx_GeneralButton usage to see if it can use a different class
// These classes were brought in from the old UserSettings and are included here to avoid
// breaking the app.

View File

@ -60,13 +60,11 @@
@import "./views/auth/_Welcome.scss";
@import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_DecoratedRoomAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_WidgetAvatar.scss";
@import "./views/beta/_BetaCard.scss";
@import "./views/context_menus/_CallContextMenu.scss";
@import "./views/context_menus/_IconizedContextMenu.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_StatusMessageContextMenu.scss";
@import "./views/context_menus/_TagTileContextMenu.scss";
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@ -237,6 +235,7 @@
@import "./views/rooms/_RoomBreadcrumbs.scss";
@import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomListHeader.scss";
@import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomSublist.scss";
@import "./views/rooms/_RoomTile.scss";

View File

@ -101,8 +101,9 @@ $roomListCollapsedWidth: 68px;
}
.mx_LeftPanel_filterContainer {
margin-left: 12px;
margin-right: 12px;
margin: 0 12px;
padding: 12px 0 8px;
border-bottom: 1px solid $quinary-content;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
@ -110,7 +111,28 @@ $roomListCollapsedWidth: 68px;
display: flex;
align-items: center;
& + .mx_RoomListHeader {
margin-top: 12px;
}
.mx_RoomSearch_shortcutPrompt {
border-radius: 6px;
background-color: $panel-actions;
padding: 2px 4px;
user-select: none;
pointer-events: none;
font-size: $font-12px;
line-height: $font-15px;
font-weight: $font-semi-bold;
color: $light-fg-color;
margin-right: 6px;
}
.mx_RoomSearch_focused, .mx_RoomSearch_hasQuery {
.mx_RoomSearch_shortcutPrompt {
display: none;
}
& + .mx_LeftPanel_exploreButton {
// Cheaty way to return the occupied space to the filter input
flex-basis: 0;
@ -170,8 +192,12 @@ $roomListCollapsedWidth: 68px;
background: $secondary-content;
}
&.mx_LeftPanel_exploreButton_space::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
&:hover {
background-color: $tertiary-content;
&::before {
background-color: $background;
}
}
}
}
@ -234,7 +260,6 @@ $roomListCollapsedWidth: 68px;
.mx_LeftPanel_exploreButton {
margin-left: 0;
margin-top: 8px;
background-color: transparent;
}
}
}

View File

@ -67,12 +67,22 @@ limitations under the License.
.mx_RoomSearch_clearButton {
width: 16px;
height: 16px;
mask-image: url('$(res)/img/element-icons/roomlist/search-clear.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background-color: $secondary-content;
margin-right: 8px;
background-color: $quinary-content;
border-radius: 50%;
position: relative;
&::before {
content: "";
position: absolute;
width: inherit;
height: inherit;
mask-image: url('$(res)/img/feather-customised/x.svg');
mask-position: center;
mask-size: 12px;
mask-repeat: no-repeat;
background-color: $secondary-content;
}
}
}
@ -82,13 +92,20 @@ limitations under the License.
}
&.mx_RoomSearch_minimized {
border-radius: 32px;
height: auto;
width: auto;
padding: 8px;
padding: 5px;
.mx_RoomSearch_icon {
margin-left: 0;
}
&:hover {
background-color: $tertiary-content;
.mx_RoomSearch_icon {
background-color: $background;
}
}
}
}

View File

@ -20,7 +20,7 @@ $gutterSize: 16px;
$activeBorderTransparentGap: 1px;
$activeBackgroundColor: $panel-actions;
$activeBorderColor: $secondary-content;
$activeBorderColor: $primary-content;
.mx_SpacePanel {
background-color: $groupFilterPanel-bg-color;
@ -37,7 +37,7 @@ $activeBorderColor: $secondary-content;
.mx_SpacePanel_spaceTreeWrapper {
flex: 1;
padding: 8px 8px 16px 0;
padding: 12px 8px 16px 0;
}
.mx_SpacePanel_toggleCollapse {
@ -92,13 +92,6 @@ $activeBorderColor: $secondary-content;
}
}
.mx_SpaceTreeLevel {
display: flex;
flex-direction: column;
max-width: 250px;
flex-grow: 1;
}
.mx_SpaceItem {
display: inline-flex;
flex-flow: wrap;
@ -209,10 +202,10 @@ $activeBorderColor: $secondary-content;
&.mx_SpaceButton_people,
&.mx_SpaceButton_orphans {
.mx_SpaceButton_icon {
background-color: #ffffff;
background-color: $panel-actions;
&::before {
background-color: #3f3d3d;
background-color: $secondary-content;
}
}
}
@ -234,8 +227,6 @@ $activeBorderColor: $secondary-content;
}
&.mx_SpaceButton_new .mx_SpaceButton_icon {
background-color: $panel-actions;
&::before {
background-color: $primary-content;
mask-image: url('$(res)/img/element-icons/plus.svg');
@ -261,7 +252,7 @@ $activeBorderColor: $secondary-content;
position: relative;
&::before {
top: 2px;
top: 3px;
left: 2px;
content: '';
width: 16px;
@ -276,6 +267,35 @@ $activeBorderColor: $secondary-content;
}
}
.mx_SpaceTreeLevel {
display: flex;
flex-direction: column;
max-width: 250px;
flex-grow: 1;
.mx_BaseAvatar:not(.mx_UserMenu_userAvatar_BaseAvatar) .mx_BaseAvatar_initial {
color: $secondary-content;
width: 32px;
height: 32px;
border-radius: 8px;
background-color: $panel-actions;
font-size: $font-15px !important; // override inline style
font-weight: $font-semi-bold;
line-height: $font-18px;
& + .mx_BaseAvatar_image {
visibility: hidden;
}
}
.mx_SpaceTreeLevel {
.mx_BaseAvatar_initial {
width: 24px;
height: 24px;
}
}
}
.mx_SpaceButton_avatarWrapper {
position: relative;
}
@ -338,13 +358,8 @@ $activeBorderColor: $secondary-content;
line-height: $font-18px;
}
.mx_IconizedContextMenu_optionList .mx_AccessibleButton.mx_SpacePanel_contextMenu_inviteButton {
color: $accent;
.mx_SpacePanel_iconInvite::before {
background-color: $accent;
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
.mx_SpacePanel_iconInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
.mx_SpacePanel_iconSettings::before {
@ -360,15 +375,11 @@ $activeBorderColor: $secondary-content;
}
.mx_SpacePanel_iconPlus::before {
mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
}
.mx_SpacePanel_iconHash::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
mask-image: url('$(res)/img/element-icons/plus.svg');
}
.mx_SpacePanel_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
mask-image: url('$(res)/img/element-icons/roomlist/search.svg');
}
.mx_SpacePanel_noIcon {
@ -378,6 +389,14 @@ $activeBorderColor: $secondary-content;
padding-left: 5px !important; // override default iconized label style to align with header
}
}
.mx_SpacePanel_contextMenu_separatorLabel {
color: $tertiary-content;
font-size: $font-10px;
line-height: $font-12px;
font-weight: $font-semi-bold;
//margin-left: 8px;
}
}
.mx_SpacePanel_sharePublicSpace {

View File

@ -85,6 +85,11 @@ limitations under the License.
background-color: $primary-content;
}
&.mx_Toast_icon_labs::after {
mask-image: url('$(res)/img/element-icons/flask.svg');
background-color: $secondary-content;
}
.mx_Toast_title, .mx_Toast_body {
grid-column: 2;
}

View File

@ -15,141 +15,51 @@ limitations under the License.
*/
.mx_UserMenu {
// to make the menu button sort of aligned with the explore button below
padding-right: 6px;
padding: 0 2px 8px;
border-bottom: 1px solid $quinary-content;
margin-left: 18px;
margin-bottom: 4px;
box-sizing: border-box;
display: flex;
align-items: center;
width: max-content;
&.mx_UserMenu_prototype {
// The margin & padding combination between here and the ::after is to
// align the border line with the tag panel.
margin-bottom: 6px;
padding-right: 0; // make the right edge line up with the explore button
.mx_UserMenu_headerButtons {
// considering we've eliminated right padding on the menu itself, we need to
// push the chevron in slightly (roughly lining up with the center of the
// plus buttons)
margin-right: 2px;
}
// we cheat opacity on the theme colour with an after selector here
&::after {
content: '';
border-bottom: 1px solid $primary-content;
opacity: 0.2;
display: block;
padding-top: 8px;
}
.mx_UserMenu_userAvatar {
position: relative;
}
.mx_UserMenu_headerButtons {
.mx_UserMenu_name {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-24px;
margin-left: 10px;
}
&.mx_UserMenu_cutout .mx_BaseAvatar {
mask-image: url('$(res)/img/element-icons/roomlist/dnd-avatar-mask.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
}
.mx_UserMenu_dndBadge {
position: absolute;
bottom: -2px;
right: -7px;
width: 16px;
height: 16px;
position: relative;
display: block;
border-radius: 50%;
&::before {
content: '';
width: 16px;
height: 16px;
position: absolute;
top: 0;
left: 0;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $tertiary-content;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
}
.mx_UserMenu_row {
// Create a row-based flexbox to ensure items stay aligned correctly.
display: flex;
align-items: center;
.mx_UserMenu_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
height: 32px; // to remove the unknown 4px gap the browser puts below it
padding: 3px 0; // to align with and without using doubleName
.mx_UserMenu_userAvatar {
border-radius: 32px; // should match avatar size
object-fit: cover;
}
}
.mx_UserMenu_doubleName {
flex: 1;
min-width: 0; // make flexbox aware that it can crush this to a tiny width
.mx_UserMenu_userName,
.mx_UserMenu_subUserName {
display: block;
}
.mx_UserMenu_subUserName {
color: $muted-fg-color;
font-size: $font-13px;
line-height: $font-18px;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.mx_UserMenu_userName {
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_UserMenu_headerButtons {
// No special styles: the rest of the layout happens to make it work.
}
.mx_UserMenu_dnd {
width: 24px;
height: 24px;
margin-right: 8px;
position: relative;
&::before {
content: '';
position: absolute;
width: 24px;
height: 24px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $muted-fg-color;
}
&.mx_UserMenu_dnd_noisy::before {
mask-image: url('$(res)/img/element-icons/notifications.svg');
}
&.mx_UserMenu_dnd_muted::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
}
}
}
&.mx_UserMenu_minimized {
padding-right: 0px;
.mx_UserMenu_userAvatarContainer {
margin-right: 0px;
background-color: $alert;
mask-image: url('$(res)/img/element-icons/roomlist/dnd.svg');
}
}
}
@ -157,44 +67,6 @@ limitations under the License.
.mx_UserMenu_contextMenu {
width: 258px;
// These override the styles already present on the user menu rather than try to
// define a new menu. They are specifically for the stacked menu when a community
// is being represented as a prototype.
&.mx_UserMenu_contextMenu_prototype {
padding-bottom: 16px;
.mx_UserMenu_contextMenu_header {
padding-bottom: 0;
padding-top: 16px;
&:nth-child(n + 2) {
padding-top: 8px;
}
}
hr {
width: 85%;
opacity: 0.2;
border: none;
border-bottom: 1px solid $primary-content;
}
&.mx_IconizedContextMenu {
> .mx_IconizedContextMenu_optionList {
margin-top: 4px;
&::before {
border: none;
}
> .mx_AccessibleButton {
padding-top: 2px;
padding-bottom: 2px;
}
}
}
}
&.mx_IconizedContextMenu .mx_IconizedContextMenu_optionList_red {
.mx_AccessibleButton {
padding-top: 16px;
@ -299,6 +171,15 @@ limitations under the License.
.mx_UserMenu_iconHome::before {
mask-image: url('$(res)/img/element-icons/roomlist/home.svg');
}
.mx_UserMenu_iconDnd::before {
mask-image: url('$(res)/img/element-icons/roomlist/dnd.svg');
}
.mx_UserMenu_iconDndOff::before {
mask-image: url('$(res)/img/element-icons/roomlist/dnd-cross.svg');
}
.mx_UserMenu_iconHosting::before {
mask-image: url('$(res)/img/element-icons/brands/element.svg');
}
@ -315,23 +196,37 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/settings.svg');
}
.mx_UserMenu_iconArchive::before {
mask-image: url('$(res)/img/element-icons/roomlist/archived.svg');
}
.mx_UserMenu_iconMessage::before {
mask-image: url('$(res)/img/element-icons/roomlist/feedback.svg');
mask-image: url('$(res)/img/element-icons/feedback.svg');
}
.mx_UserMenu_iconSignOut::before {
mask-image: url('$(res)/img/element-icons/leave.svg');
}
}
.mx_UserMenu_iconMembers::before {
mask-image: url('$(res)/img/element-icons/room/members.svg');
.mx_UserMenu_CustomStatusSection {
margin: 0 12px 8px;
.mx_UserMenu_CustomStatusSection_input {
position: relative;
display: flex;
> input {
border: 1px solid $accent;
border-radius: 8px;
width: 100%;
}
}
.mx_UserMenu_iconInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
> p {
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-content;
margin: 4px 0;
}
.mx_AccessibleButton_kind_primary_outline {
display: block;
}
}

View File

@ -1,29 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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_MessageComposer_avatar .mx_BaseAvatar {
padding: 2px;
border: 1px solid transparent;
border-radius: 100%;
}
.mx_MessageComposer_avatar .mx_BaseAvatar_initial {
left: 2px;
}
.mx_MemberStatusMessageAvatar_hasStatus .mx_BaseAvatar {
border-color: $accent;
}

View File

@ -59,6 +59,11 @@ limitations under the License.
border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu
}
// round all corners of the only button for the hover effect to be bounded
&:first-child:last-child .mx_AccessibleButton:first-child:last-child {
border-radius: 8px; // radius matches .mx_ContextualMenu
}
.mx_AccessibleButton {
// pad the inside of the button so that the hover background is padded too
padding-top: 12px;

View File

@ -1,65 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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_StatusMessageContextMenu {
padding: 10px;
}
.mx_StatusMessageContextMenu_form {
display: flex;
flex-direction: column;
}
input.mx_StatusMessageContextMenu_message {
border-radius: 4px;
border: 1px solid $input-border-color;
padding: 6.5px 11px;
background-color: $background;
font-weight: normal;
margin: 0 0 10px;
}
.mx_StatusMessageContextMenu_message::placeholder {
color: $tertiary-content;
}
.mx_StatusMessageContextMenu_actionContainer {
display: flex;
}
.mx_StatusMessageContextMenu_submit,
.mx_StatusMessageContextMenu_clear {
@mixin mx_DialogButton;
align-self: start;
font-size: $font-12px;
padding: 6px 1em;
border: 1px solid transparent;
margin-right: 10px;
}
.mx_StatusMessageContextMenu_submit[disabled] {
opacity: 0.49;
}
.mx_StatusMessageContextMenu_clear {
color: $alert;
background-color: transparent;
border: 1px solid $alert;
}
.mx_StatusMessageContextMenu_actionContainer .mx_Spinner {
justify-content: flex-start;
}

View File

@ -30,10 +30,32 @@ limitations under the License.
.mx_FeedbackDialog_section {
position: relative;
padding-left: 52px;
padding: 12px 52px 12px 68px;
border-radius: 8px;
border: 1px solid $quinary-content;
& + .mx_FeedbackDialog_section {
margin-top: 16px;
}
> h3 {
margin-top: 0;
margin-bottom: 8px;
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
}
> p {
color: $tertiary-content;
color: $secondary-content;
font-size: $font-15px;
line-height: $font-24px;
margin: 8px 0 0;
}
.mx_FeedbackDialog_section_microcopy {
font-size: $font-12px;
line-height: $font-15px;
}
.mx_AccessibleButton_kind_link {
@ -51,13 +73,13 @@ limitations under the License.
position: absolute;
width: 40px;
height: 40px;
left: 0;
top: 0;
left: 16px;
top: 12px;
}
&::before {
background-color: $icon-button-color;
border-radius: 20px;
border-radius: 8px;
}
&::after {

View File

@ -20,7 +20,7 @@ limitations under the License.
margin: auto;
border-top: 1px solid $primary-hairline-color;
position: relative;
padding-left: 82px;
padding-left: 42px;
padding-right: 6px;
}
@ -74,7 +74,7 @@ limitations under the License.
.mx_MessageComposer_e2eIcon.mx_E2EIcon {
position: absolute;
left: 60px;
left: 20px;
margin-right: 0; // Counteract the E2EIcon class
margin-left: 3px; // Counteract the E2EIcon class
width: 12px;

View File

@ -21,6 +21,7 @@ limitations under the License.
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 12px;
.mx_RoomBreadcrumbs_crumb {
margin-right: 8px;

View File

@ -21,18 +21,24 @@ limitations under the License.
.mx_RoomList_iconPlus::before {
mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
}
.mx_RoomList_iconHash::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
.mx_RoomList_iconCreateNewRoom::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-plus.svg');
}
.mx_RoomList_iconAddExistingRoom::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash.svg');
}
.mx_RoomList_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
.mx_RoomList_iconBrowse::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
mask-image: url('$(res)/img/element-icons/roomlist/hash-search.svg');
}
.mx_RoomList_iconDialpad::before {
mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg');
}
.mx_RoomList_iconStartChat {
mask-image: url('$(res)/img/element-icons/roomlist/member-plus.svg');
}
.mx_RoomList_iconInvite {
mask-image: url('$(res)/img/element-icons/room/share.svg');
}
.mx_RoomList_explorePrompt {
margin: 4px 12px 4px;
@ -77,13 +83,5 @@ limitations under the License.
&.mx_RoomList_explorePrompt_explore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
&.mx_RoomList_explorePrompt_spaceInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
&.mx_RoomList_explorePrompt_spaceExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
}
}

View File

@ -0,0 +1,111 @@
/*
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_RoomListHeader {
display: flex;
align-items: center;
.mx_RoomListHeader_contextLessTitle,
.mx_RoomListHeader_contextMenuButton {
font-size: $font-15px;
line-height: $font-24px;
font-weight: $font-semi-bold;
padding: 1px 24px 1px 4px;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 8px;
margin-right: auto;
user-select: none;
}
.mx_RoomListHeader_contextMenuButton {
border-radius: 6px;
&:hover {
background-color: $quinary-content;
}
&::before {
content: '';
width: 20px;
height: 20px;
top: 3px;
right: 0;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background-color: $tertiary-content;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
&[aria-expanded=true] {
background-color: $quinary-content;
&::before {
transform: rotate(180deg);
}
}
}
.mx_RoomListHeader_plusButton {
width: 32px;
height: 32px;
border-radius: 8px;
position: relative;
padding: 8px;
margin-left: 8px;
margin-right: 12px;
background-color: $panel-actions;
box-sizing: border-box;
flex-shrink: 0;
&::before {
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background-color: $secondary-content;
mask-image: url('$(res)/img/element-icons/roomlist/plus.svg');
}
&:hover {
background-color: $tertiary-content;
&::before {
background-color: $background;
}
}
}
}
.mx_RoomListHeader_iconInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
.mx_RoomListHeader_iconStartChat::before {
mask-image: url('$(res)/img/element-icons/roomlist/member-plus.svg');
}
.mx_RoomListHeader_iconCreateRoom::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-plus.svg');
}
.mx_RoomListHeader_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-search.svg');
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 2.5C13 2.77614 12.7761 3 12.5 3C12.2239 3 12 2.77614 12 2.5C12 2.22386 12.2239 2 12.5 2C12.7761 2 13 2.22386 13 2.5ZM11 5C11 5.55228 10.5523 6 10 6C9.44772 6 9 5.55228 9 5C9 4.44772 9.44772 4 10 4C10.5523 4 11 4.44772 11 5ZM14.7778 11.3056L18.6333 18.2389C18.8466 18.6247 18.9556 19.0594 18.9495 19.5002C18.9434 19.9409 18.8225 20.3725 18.5987 20.7522C18.3749 21.132 18.0559 21.4468 17.6732 21.6656C17.2906 21.8845 16.8575 21.9997 16.4167 22H7.58334C7.14253 21.9997 6.70943 21.8845 6.32677 21.6656C5.94412 21.4468 5.62514 21.132 5.40132 20.7522C5.17751 20.3725 5.0566 19.9409 5.05052 19.5002C5.04444 19.0594 5.15341 18.6247 5.36667 18.2389L9.22222 11.3056L9.19445 8.11111H8.66667C8.51933 8.11111 8.37802 8.05258 8.27383 7.94839C8.16965 7.84421 8.11111 7.7029 8.11111 7.55556C8.11111 7.40821 8.16965 7.26691 8.27383 7.16272C8.37802 7.05853 8.51933 7 8.66667 7H15.3333C15.4807 7 15.622 7.05853 15.7262 7.16272C15.8304 7.26691 15.8889 7.40821 15.8889 7.55556C15.8889 7.7029 15.8304 7.84421 15.7262 7.94839C15.622 8.05258 15.4807 8.11111 15.3333 8.11111H14.8056L14.7778 11.3056ZM10.3333 11.4389C10.3338 11.5362 10.3089 11.6319 10.2611 11.7167L8.88334 14.2C10.0819 14.237 11.271 14.4256 12.4222 14.7611C13.4944 15.0808 14.6039 15.2583 15.7222 15.2889L13.7389 11.7167C13.6911 11.6319 13.6662 11.5362 13.6667 11.4389L13.6944 8.11111H10.3056L10.3333 11.4389ZM14.5 5C14.7761 5 15 4.77614 15 4.5C15 4.22386 14.7761 4 14.5 4C14.2239 4 14 4.22386 14 4.5C14 4.77614 14.2239 5 14.5 5Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.9999 16.0494C32 16.0329 32 16.0165 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32C18.1985 32 20.2935 31.5566 22.2003 30.7543C21.4347 29.3403 21 27.7209 21 26C21 20.4772 25.4772 16 31 16C31.3375 16 31.671 16.0167 31.9999 16.0494Z" fill="#C1C6CD"/>
</svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 7.3335V8.66683H10.5533L13.56 11.6735C14.26 10.6202 14.6667 9.36016 14.6667 8.00016C14.6667 4.32016 11.68 1.3335 8.00001 1.3335C6.64001 1.3335 5.38001 1.74016 4.32668 2.44016L9.22001 7.3335H11.3333ZM0.926682 2.8135L2.44001 4.32683C1.74001 5.38016 1.33335 6.64016 1.33335 8.00016C1.33335 11.6802 4.32001 14.6668 8.00001 14.6668C9.36001 14.6668 10.62 14.2602 11.6733 13.5602L13.1867 15.0735L14.1267 14.1335L1.87335 1.8735L0.926682 2.8135ZM4.66668 7.3335H5.44668L6.78001 8.66683H4.66668V7.3335Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 630 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM17 13H7V11H17V13Z" fill="#17191C"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@ -1,7 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9C17 13.4183 13.4183 17 9 17C4.58172 17 1 13.4183 1 9C1 4.58172 4.58172 1 9 1C13.4183 1 17 4.58172 17 9ZM6.85231 7.13323L8.0052 5.98033C8.2981 5.68744 8.2981 5.21256 8.0052 4.91967C7.71231 4.62678 7.23744 4.62678 6.94454 4.91967L4.46967 7.39454C4.17678 7.68744 4.17678 8.16231 4.46967 8.4552L6.94454 10.9301C7.23744 11.223 7.71231 11.223 8.0052 10.9301C8.2981 10.6372 8.2981 10.1623 8.0052 9.86942L6.76901 8.63323H10.3537C11.0454 8.63323 11.725 9.27325 11.725 10.2166C11.725 11.1599 11.0454 11.7999 10.3537 11.7999H9.36384C8.94962 11.7999 8.61384 12.1357 8.61384 12.5499C8.61384 12.9641 8.94962 13.2999 9.36384 13.2999H10.3537C12.0051 13.2999 13.225 11.8506 13.225 10.2166C13.225 8.58255 12.0051 7.13323 10.3537 7.13323H6.85231Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9C17 13.4183 13.4183 17 9 17C4.58172 17 1 13.4183 1 9C1 4.58172 4.58172 1 9 1C13.4183 1 17 4.58172 17 9ZM6.85231 7.13323L8.0052 5.98033C8.2981 5.68744 8.2981 5.21256 8.0052 4.91967C7.71231 4.62678 7.23744 4.62678 6.94454 4.91967L4.46967 7.39454C4.17678 7.68744 4.17678 8.16231 4.46967 8.4552L6.94454 10.9301C7.23744 11.223 7.71231 11.223 8.0052 10.9301C8.2981 10.6372 8.2981 10.1623 8.0052 9.86942L6.76901 8.63323H10.3537C11.0454 8.63323 11.725 9.27325 11.725 10.2166C11.725 11.1599 11.0454 11.7999 10.3537 11.7999H9.36384C8.94962 11.7999 8.61384 12.1357 8.61384 12.5499C8.61384 12.9641 8.94962 13.2999 9.36384 13.2999H10.3537C12.0051 13.2999 13.225 11.8506 13.225 10.2166C13.225 8.58255 12.0051 7.13323 10.3537 7.13323H6.85231Z" fill="black"/>
<path d="M8.0052 5.98033L7.29995 5.27508L7.29995 5.27508L8.0052 5.98033ZM6.85231 7.13323L6.14705 6.42797L4.44442 8.13061H6.85231V7.13323ZM6.94454 4.91967L7.6498 5.62492L6.94454 4.91967ZM4.46967 7.39454L3.76442 6.68929L3.76442 6.68929L4.46967 7.39454ZM4.46967 8.4552L3.76442 9.16046H3.76442L4.46967 8.4552ZM6.94454 10.9301L7.6498 10.2248L7.6498 10.2248L6.94454 10.9301ZM6.76901 8.63323V7.63585H4.36113L6.06376 9.33848L6.76901 8.63323ZM9 17.9974C13.9691 17.9974 17.9974 13.9691 17.9974 9H16.0026C16.0026 12.8674 12.8674 16.0026 9 16.0026V17.9974ZM0.00262225 9C0.00262225 13.9691 4.03089 17.9974 9 17.9974V16.0026C5.13256 16.0026 1.99738 12.8674 1.99738 9H0.00262225ZM9 0.00262225C4.03089 0.00262225 0.00262225 4.03089 0.00262225 9H1.99738C1.99738 5.13256 5.13256 1.99738 9 1.99738V0.00262225ZM17.9974 9C17.9974 4.03089 13.9691 0.00262225 9 0.00262225V1.99738C12.8674 1.99738 16.0026 5.13256 16.0026 9H17.9974ZM7.29995 5.27508L6.14705 6.42797L7.55756 7.83848L8.71046 6.68558L7.29995 5.27508ZM7.29995 5.62492C7.20334 5.52832 7.20334 5.37168 7.29995 5.27508L8.71046 6.68558C9.39285 6.00319 9.39285 4.89681 8.71046 4.21442L7.29995 5.62492ZM7.6498 5.62492C7.55319 5.72153 7.39656 5.72153 7.29995 5.62492L8.71046 4.21442C8.02806 3.53202 6.92168 3.53202 6.23929 4.21442L7.6498 5.62492ZM5.17492 8.0998L7.6498 5.62492L6.23929 4.21442L3.76442 6.68929L5.17492 8.0998ZM5.17492 7.74995C5.27153 7.84656 5.27153 8.00319 5.17492 8.0998L3.76442 6.68929C3.08202 7.37169 3.08202 8.47806 3.76442 9.16046L5.17492 7.74995ZM7.6498 10.2248L5.17492 7.74995L3.76442 9.16046L6.23929 11.6353L7.6498 10.2248ZM7.29995 10.2248C7.39656 10.1282 7.55319 10.1282 7.6498 10.2248L6.23929 11.6353C6.92168 12.3177 8.02806 12.3177 8.71046 11.6353L7.29995 10.2248ZM7.29995 10.5747C7.20334 10.4781 7.20334 10.3214 7.29995 10.2248L8.71046 11.6353C9.39285 10.9529 9.39285 9.84656 8.71046 9.16416L7.29995 10.5747ZM6.06376 9.33848L7.29995 10.5747L8.71046 9.16416L7.47427 7.92797L6.06376 9.33848ZM6.76901 9.6306H10.3537V7.63585H6.76901V9.6306ZM10.3537 9.6306C10.4202 9.6306 10.7276 9.74669 10.7276 10.2166H12.7223C12.7223 8.79981 11.6705 7.63585 10.3537 7.63585V9.6306ZM10.7276 10.2166C10.7276 10.6865 10.4202 10.8025 10.3537 10.8025V12.7973C11.6705 12.7973 12.7223 11.6333 12.7223 10.2166H10.7276ZM10.3537 10.8025H9.36384V12.7973H10.3537V10.8025ZM9.36384 10.8025C8.39879 10.8025 7.61646 11.5849 7.61646 12.5499H9.61121C9.61121 12.6865 9.50046 12.7973 9.36384 12.7973V10.8025ZM7.61646 12.5499C7.61646 13.515 8.39879 14.2973 9.36384 14.2973V12.3025C9.50046 12.3025 9.61121 12.4133 9.61121 12.5499H7.61646ZM9.36384 14.2973H10.3537V12.3025H9.36384V14.2973ZM10.3537 14.2973C12.6497 14.2973 14.2223 12.3028 14.2223 10.2166H12.2276C12.2276 11.3984 11.3606 12.3025 10.3537 12.3025V14.2973ZM14.2223 10.2166C14.2223 8.13036 12.6497 6.13585 10.3537 6.13585V8.13061C11.3606 8.13061 12.2276 9.03475 12.2276 10.2166H14.2223ZM10.3537 6.13585H6.85231V8.13061H10.3537V6.13585Z" fill="black" mask="url(#path-1-inside-1)"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.393 1.00573C11.9421 1.06434 12.3398 1.55703 12.2812 2.1062L12.0245 4.51168H13.8482C14.4005 4.51168 14.8482 4.95939 14.8482 5.51168C14.8482 6.06396 14.4005 6.51168 13.8482 6.51168H13.0089C12.5534 5.89782 11.8231 5.5 11 5.5C10.1769 5.5 9.44663 5.89782 8.9911 6.51168H6.36673L6.05542 9.42868C5.70806 9.85802 5.5 10.4047 5.5 11C5.5 11.4052 5.5964 11.7879 5.76753 12.1264L5.55616 14.107C5.49755 14.6562 5.00485 15.0538 4.45569 14.9952C3.90652 14.9366 3.50884 14.4439 3.56745 13.8948L3.79318 11.7796H2.0005C1.44822 11.7796 1.0005 11.3319 1.0005 10.7796C1.0005 10.2273 1.44822 9.77962 2.0005 9.77962H4.00661L4.35537 6.51168H2.64188C2.0896 6.51168 1.64188 6.06396 1.64188 5.51168C1.64188 4.95939 2.0896 4.51168 2.64188 4.51168H4.56881L4.84817 1.89397C4.90677 1.3448 5.39947 0.947122 5.94864 1.00573C6.49781 1.06434 6.89548 1.55703 6.83688 2.1062L6.58016 4.51168H10.0131L10.2925 1.89397C10.3511 1.3448 10.8438 0.947122 11.393 1.00573Z" fill="#737D8C"/>
<path d="M8 11H14" stroke="#737D8C" stroke-width="2" stroke-linecap="round"/>
<path d="M11 8L11 14" stroke="#737D8C" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.393 1.0028C10.9421 1.06141 11.3398 1.5541 11.2812 2.10327L11.0245 4.50875H12.8482C13.4005 4.50875 13.8482 4.95647 13.8482 5.50875C13.8482 6.06103 13.4005 6.50875 12.8482 6.50875H12.6234C12.0135 6.18307 11.3277 6.00024 10.6028 6.00024C9.87794 6.00024 9.19219 6.18307 8.58222 6.50875H5.36673L5.01797 9.77669H6.14293C6.04993 10.1746 6.0005 10.5915 6.0005 11.0209C6.0005 11.2778 6.01819 11.5302 6.0523 11.7767H4.80453L4.55616 14.1041C4.49755 14.6532 4.00485 15.0509 3.45569 14.9923C2.90652 14.9337 2.50884 14.441 2.56745 13.8918L2.79318 11.7767H1.0005C0.448219 11.7767 0.00050354 11.329 0.00050354 10.7767C0.00050354 10.2244 0.448219 9.77669 1.0005 9.77669H3.00661L3.35537 6.50875H1.64188C1.0896 6.50875 0.64188 6.06103 0.64188 5.50875C0.64188 4.95647 1.0896 4.50875 1.64188 4.50875H3.56881L3.84817 1.89104C3.90677 1.34187 4.39947 0.944193 4.94864 1.0028C5.49781 1.06141 5.89548 1.5541 5.83688 2.10327L5.58016 4.50875H9.01315L9.29251 1.89104C9.35111 1.34187 9.84381 0.944193 10.393 1.0028Z" fill="#737D8C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3731 10.6811C12.3731 11.6155 11.6156 12.3731 10.6811 12.3731C9.74669 12.3731 8.98917 11.6155 8.98917 10.6811C8.98917 9.74667 9.74669 8.98915 10.6811 8.98915C11.6156 8.98915 12.3731 9.74667 12.3731 10.6811ZM13.5542 12.3993C13.8552 11.897 14.0283 11.3093 14.0283 10.6811C14.0283 8.83254 12.5297 7.33398 10.6811 7.33398C8.83255 7.33398 7.33398 8.83254 7.33398 10.6811C7.33398 12.5297 8.83255 14.0282 10.6811 14.0282C11.3093 14.0282 11.897 13.8552 12.3992 13.5542C12.4251 13.5903 12.4543 13.6247 12.4867 13.6571L13.9212 15.0916C14.2444 15.4148 14.7684 15.4148 15.0916 15.0916C15.4148 14.7684 15.4148 14.2444 15.0916 13.9212L13.6571 12.4867C13.6247 12.4543 13.5903 12.4252 13.5542 12.3993Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.82942 1.08081C6.8741 0.622751 6.539 0.215191 6.08094 0.170502C5.62287 0.125813 5.21531 0.460918 5.17062 0.918981L4.85379 4.16656H2.66669C2.20645 4.16656 1.83335 4.53966 1.83335 4.9999C1.83335 5.46013 2.20645 5.83323 2.66669 5.83323H4.69119L4.26842 10.1666H2.00002C1.53978 10.1666 1.16669 10.5397 1.16669 10.9999C1.16669 11.4601 1.53978 11.8332 2.00002 11.8332H4.10582L3.83729 14.5856C3.7926 15.0437 4.12771 15.4513 4.58577 15.496C5.04383 15.5406 5.45139 15.2055 5.49608 14.7475L5.7804 11.8332H9.77249L9.50396 14.5856C9.45927 15.0437 9.79437 15.4513 10.2524 15.496C10.7105 15.5406 11.1181 15.2055 11.1627 14.7475L11.4471 11.8332H14.3334C14.7936 11.8332 15.1667 11.4601 15.1667 10.9999C15.1667 10.5397 14.7936 10.1666 14.3334 10.1666H11.6097L12.0324 5.83323H14.3334C14.7936 5.83323 15.1667 5.46013 15.1667 4.9999C15.1667 4.53966 14.7936 4.16656 14.3334 4.16656H12.195L12.4961 1.08081C12.5408 0.622751 12.2057 0.215191 11.7476 0.170502C11.2895 0.125813 10.882 0.460918 10.8373 0.918981L10.5205 4.16656H6.52837L6.82942 1.08081ZM9.93509 10.1666L10.3579 5.83323H6.36577L5.943 10.1666H9.93509Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3831 1C11.7973 1 12.1331 1.33579 12.1331 1.75V3.30005L13.5833 3.30005C13.9975 3.30005 14.3333 3.63584 14.3333 4.05005C14.3333 4.46426 13.9975 4.80005 13.5833 4.80005L12.1331 4.80005V6.25C12.1331 6.66421 11.7973 7 11.3831 7C10.9689 7 10.6331 6.66421 10.6331 6.25V4.80005L9.08331 4.80005C8.6691 4.80005 8.33331 4.46426 8.33331 4.05005C8.33331 3.63583 8.6691 3.30005 9.08331 3.30005L10.6331 3.30005V1.75C10.6331 1.33579 10.9689 1 11.3831 1ZM5.74076 9.00014C6.88624 9.00014 7.81483 8.07155 7.81483 6.92607C7.81483 5.78059 6.88624 4.85199 5.74076 4.85199C4.59528 4.85199 3.66669 5.78059 3.66669 6.92607C3.66669 8.07155 4.59528 9.00014 5.74076 9.00014ZM5.88889 10.7778C3.00889 10.7778 1 12.2415 1 14.3333V14.926H10.7778V14.3333C10.7778 12.2356 8.76889 10.7778 5.88889 10.7778Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 945 B

View File

@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 16C12.866 16 16 12.866 16 9C16 5.13401 12.866 2 9 2C5.13401 2 2 5.13401 2 9C2 12.866 5.13401 16 9 16ZM5.96967 5.96967C6.26256 5.67678 6.73744 5.67678 7.03033 5.96967L9 7.93934L10.9697 5.96967C11.2626 5.67678 11.7374 5.67678 12.0303 5.96967C12.3232 6.26256 12.3232 6.73744 12.0303 7.03033L10.0607 9L12.0303 10.9697C12.3232 11.2626 12.3232 11.7374 12.0303 12.0303C11.7374 12.3232 11.2626 12.3232 10.9697 12.0303L9 10.0607L7.03033 12.0303C6.73744 12.3232 6.26256 12.3232 5.96967 12.0303C5.67678 11.7374 5.67678 11.2626 5.96967 10.9697L7.93934 9L5.96967 7.03033C5.67678 6.73744 5.67678 6.26256 5.96967 5.96967Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -24,7 +24,6 @@ import RoomList from "../views/rooms/RoomList";
import CallHandler from "../../CallHandler";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions";
import UserMenu from "./UserMenu";
import RoomSearch from "./RoomSearch";
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
@ -33,15 +32,14 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomListHeader from "../views/rooms/RoomListHeader";
import { Key } from "../../Keyboard";
interface IProps {
@ -63,9 +61,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private focusedElement = null;
private isDoingStickyHeaders = false;
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps) {
super(props);
@ -327,14 +322,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
};
private renderHeader(): React.ReactNode {
return (
<div className="mx_LeftPanel_userHeader">
<UserMenu isMinimized={this.props.isMinimized} />
</div>
);
}
private renderBreadcrumbs(): React.ReactNode {
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
return (
@ -362,7 +349,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
/>;
}
const space = this.state.activeSpace[0] === "!" ? this.context.getRoom(this.state.activeSpace) : null;
return (
<div
className="mx_LeftPanel_filterContainer"
@ -378,13 +364,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{ dialPadButton }
<AccessibleTooltipButton
className={classNames("mx_LeftPanel_exploreButton", {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
})}
{ this.state.activeSpace === MetaSpace.Home && <AccessibleTooltipButton
className="mx_LeftPanel_exploreButton"
onClick={this.onExplore}
title={space ? _t("Explore %(spaceName)s", { spaceName: space.name }) : _t("Explore rooms")}
/>
title={_t("Explore rooms")}
/> }
</div>
);
}
@ -415,10 +399,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return (
<div className={containerClasses} ref={this.ref}>
<aside className="mx_LeftPanel_roomListContainer">
{ this.renderHeader() }
{ this.renderSearchDialExplore() }
{ this.renderBreadcrumbs() }
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
{ !this.props.isMinimized && <RoomListHeader onVisibilityChange={this.refreshStickyHeaders} /> }
<div className="mx_LeftPanel_roomListWrapper">
<div
className={roomListClasses}

View File

@ -82,7 +82,6 @@ import SpaceStore from "../../stores/spaces/SpaceStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
@ -112,6 +111,10 @@ import { initSentry } from "../../sentry";
import { logger } from "matrix-js-sdk/src/logger";
import { showSpaceInvite } from "../../utils/space";
import GenericToast from "../views/toasts/GenericToast";
import InfoDialog from "../views/dialogs/InfoDialog";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import AccessibleButton from "../views/elements/AccessibleButton";
/** constants for MatrixChat.state.view */
export enum Views {
@ -712,16 +715,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break;
}
case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace[0] === "!") {
defaultDispatcher.dispatch({
action: "view_room",
room_id: SpaceStore.instance.activeSpace,
});
} else {
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText,
}, 'mx_RoomDirectory_dialogWrapper', false, true);
}
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText,
}, 'mx_RoomDirectory_dialogWrapper', false, true);
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
@ -1476,6 +1472,61 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
showNotificationsToast(false);
}
if (!localStorage.getItem("mx_seen_ia_1.1_changes_toast")) {
const key = "IA_1.1_TOAST";
ToastStore.sharedInstance().addOrReplaceToast({
key,
title: _t("Testing small changes"),
props: {
description: _t("Your feedback is wanted as we try out some design changes."),
acceptLabel: _t("More info"),
onAccept: () => {
Modal.createDialog(InfoDialog, {
title: _t("We're testing some design changes"),
description: <>
<img
src={require("../../../res/img/ia-design-changes.png")}
width="636"
height="303"
alt=""
/>
<p>{ _t(
"Your ongoing feedback would be very welcome, so if you see anything " +
"different you want to comment on, <a>please let us know about it</a>. " +
"Click your avatar to find a quick feedback link.",
{},
{
a: sub => <AccessibleButton
kind="link"
onClick={(ev) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
}}
>
{ sub }
</AccessibleButton>,
},
) }</p>
<p>{ _t("If you'd like to preview or test some potential upcoming changes, " +
"there's an option in feedback to let us contact you.") }</p>
</>,
}, "mx_DialogDesignChanges_wrapper");
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
ToastStore.sharedInstance().dismissToast(key);
},
rejectLabel: _t("Dismiss"),
onReject: () => {
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
ToastStore.sharedInstance().dismissToast(key);
},
},
icon: "labs",
component: GenericToast,
priority: 9,
});
}
dis.fire(Action.FocusSendMessageComposer);
this.setState({
ready: true,

View File

@ -17,7 +17,6 @@ limitations under the License.
import * as React from "react";
import { createRef } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
@ -29,7 +28,8 @@ import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCo
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/spaces";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { isMac } from "../../Keyboard";
interface IProps {
isMinimized: boolean;
@ -42,7 +42,6 @@ interface IProps {
interface IState {
query: string;
focused: boolean;
inSpaces: boolean;
}
@replaceableComponent("structures.RoomSearch")
@ -57,13 +56,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
this.state = {
query: "",
focused: false,
inSpaces: false,
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@ -84,15 +81,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
}
private onSpaces = (spaces: Room[]) => {
this.setState({
inSpaces: spaces.length > 0,
});
};
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.ViewRoom && payload.clear_search) {
this.clearInput();
@ -163,11 +153,6 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
});
let placeholder = _t("Filter");
if (this.state.inSpaces) {
placeholder = _t("Filter all spaces");
}
let icon = (
<div className='mx_RoomSearch_icon' />
);
@ -181,7 +166,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onBlur={this.onBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
placeholder={placeholder}
placeholder={_t("Filter")}
autoComplete="off"
/>
);
@ -193,6 +178,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onClick={this.clearInput}
/>
);
let shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt">
{ isMac ? "⌘ K" : "Ctrl K" }
</div>;
if (this.props.isMinimized) {
icon = (
@ -204,12 +192,14 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
);
input = null;
clearButton = null;
shortcutPrompt = null;
}
return (
<div className={classes}>
{ icon }
{ input }
{ shortcutPrompt }
{ clearButton }
</div>
);

View File

@ -48,6 +48,7 @@ import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPan
import { useStateArray } from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare";
import {
shouldShowSpaceInvite,
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
@ -396,7 +397,7 @@ const SpaceLandingAddButton = ({ space }) => {
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
iconClassName="mx_RoomList_iconAddExistingRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -440,9 +441,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
const userId = cli.getUserId();
let inviteButton;
if (((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) &&
shouldShowComponent(UIComponent.InviteUsers)
) {
if (shouldShowSpaceInvite(space) && shouldShowComponent(UIComponent.InviteUsers)) {
inviteButton = (
<AccessibleButton
kind="primary"

View File

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from "react";
import React, { createRef, useContext, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import * as fbEmitter from "fbemitter";
import classNames from "classnames";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
@ -25,7 +25,7 @@ import dis from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
import { ContextMenuButton } from "./ContextMenu";
import { ChevronFace, ContextMenuButton } from "./ContextMenu";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
@ -42,27 +42,62 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { SettingLevel } from "../../settings/SettingLevel";
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import { showCommunityInviteDialog } from "../../RoomInvite";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import RoomName from "../views/elements/RoomName";
import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton";
import { logger } from "matrix-js-sdk/src/logger";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
const CustomStatusSection = () => {
const cli = useContext(MatrixClientContext);
const setStatus = cli.getUser(cli.getUserId()).unstable_statusMessage || "";
const [value, setValue] = useState(setStatus);
let details: JSX.Element;
if (value !== setStatus) {
details = <>
<p>{ _t("Your status will be shown to people you have a DM with.") }</p>
<AccessibleButton
onClick={() => cli._unstable_setStatusMessage(value)}
kind="primary_outline"
>
{ value ? _t("Set status") : _t("Clear status") }
</AccessibleButton>
</>;
}
return <div className="mx_UserMenu_CustomStatusSection">
<div className="mx_UserMenu_CustomStatusSection_input">
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder={_t("Set a new status")}
autoComplete="off"
/>
<AccessibleButton
tabIndex={-1}
title={_t("Clear")}
className="mx_UserMenu_CustomStatusSection_clear"
onClick={() => setValue("")}
/>
</div>
{ details }
</div>;
};
interface IProps {
isMinimized: boolean;
isPanelCollapsed: boolean;
}
type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
@ -72,14 +107,30 @@ interface IState {
isDarkTheme: boolean;
isHighContrast: boolean;
selectedSpace?: Room;
pendingRoomJoin: Set<string>;
dndEnabled: boolean;
}
const toRightOf = (rect: PartialDOMRect) => {
return {
left: rect.width + rect.left + 8,
top: rect.top,
chevronFace: ChevronFace.None,
};
};
const below = (rect: PartialDOMRect) => {
return {
left: rect.left,
top: rect.top + rect.height,
chevronFace: ChevronFace.None,
};
};
@replaceableComponent("structures.UserMenu")
export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;
private dndWatcherRef: string;
private readonly dndWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private tagStoreRef: fbEmitter.EventSubscription;
@ -90,7 +141,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(),
pendingRoomJoin: new Set<string>(),
dndEnabled: this.doNotDisturb,
selectedSpace: SpaceStore.instance.activeSpaceRoom,
};
@ -99,8 +150,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
// Force update is the easiest way to trigger the UI update (we don't store state for this)
this.dndWatcherRef = SettingsStore.watchSetting("doNotDisturb", null, () => this.forceUpdate());
SettingsStore.monitorSetting("feature_dnd", null);
SettingsStore.monitorSetting("doNotDisturb", null);
}
private get doNotDisturb(): boolean {
return SettingsStore.getValue("doNotDisturb");
}
private get hasHomePage(): boolean {
@ -111,7 +166,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
MatrixClientPeg.get().on("Room", this.onRoom);
}
public componentWillUnmount() {
@ -123,13 +177,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (SpaceStore.spacesEnabled) {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
MatrixClientPeg.get().removeListener("Room", this.onRoom);
}
private onRoom = (room: Room): void => {
this.removePendingJoinRoom(room.roomId);
};
private onTagStoreUpdate = () => {
this.forceUpdate(); // we don't have anything useful in state to update
};
@ -178,8 +227,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
});
};
private onAction = (ev: ActionPayload) => {
switch (ev.action) {
private onAction = (payload: ActionPayload) => {
switch (payload.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) {
this.setState({ contextMenuPosition: null });
@ -187,36 +236,27 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (this.buttonRef.current) this.buttonRef.current.click();
}
break;
case Action.JoinRoom:
this.addPendingJoinRoom(ev.roomId);
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
this.removePendingJoinRoom(ev.roomId);
break;
case Action.SettingUpdated: {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
switch (settingUpdatedPayload.settingName) {
case "feature_dnd":
case "doNotDisturb": {
const dndEnabled = this.doNotDisturb;
if (this.state.dndEnabled !== dndEnabled) {
this.setState({ dndEnabled });
}
break;
}
}
}
}
};
private addPendingJoinRoom(roomId: string): void {
this.setState({
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
.add(roomId),
});
}
private removePendingJoinRoom(roomId: string): void {
if (this.state.pendingRoomJoin.delete(roomId)) {
this.setState({
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
});
}
}
private onOpenMenuClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
this.setState({ contextMenuPosition: ev.currentTarget.getBoundingClientRect() });
};
private onContextMenu = (ev: React.MouseEvent) => {
@ -262,15 +302,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onShowArchived = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038
// Note: You'll need to uncomment the button too.
logger.log("TODO: Show archived rooms");
};
private onProvideFeedback = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -312,50 +343,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunitySettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// We'd ideally just pop open a right panel with the member list, but the current
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
// switch to the general room and open the member list there as it should be in sync
// anyways.
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat) {
dis.dispatch({
action: Action.ViewRoom,
room_id: chat.roomId,
}, true);
dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
title: _t('Failed to find the general chat for this community'),
description: _t("Failed to find the general chat for this community"),
});
}
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onDndToggle = (ev) => {
private onDndToggle = (ev: ButtonEvent) => {
ev.stopPropagation();
const current = SettingsStore.getValue("doNotDisturb");
SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
@ -364,8 +352,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null;
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
let topSection;
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
if (MatrixClientPeg.get().isGuest()) {
@ -411,6 +397,24 @@ export default class UserMenu extends React.Component<IProps, IState> {
);
}
let customStatusSection: JSX.Element;
if (SettingsStore.getValue("feature_custom_status")) {
customStatusSection = <CustomStatusSection />;
}
let dndButton: JSX.Element;
if (SettingsStore.getValue("feature_dnd")) {
dndButton = (
<IconizedContextMenuCheckbox
iconClassName={this.state.dndEnabled ? "mx_UserMenu_iconDnd" : "mx_UserMenu_iconDndOff"}
label={_t("Do not disturb")}
onClick={this.onDndToggle}
active={this.state.dndEnabled}
words
/>
);
}
let feedbackButton;
if (SettingsStore.getValue(UIFeature.Feedback)) {
feedbackButton = <IconizedContextMenuOption
@ -420,156 +424,68 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>;
}
let primaryHeader = (
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ OwnProfileStore.instance.displayName }
</span>
<span className="mx_UserMenu_contextMenu_userId">
{ MatrixClientPeg.get().getUserId() }
</span>
</div>
);
let primaryOptionList = (
<React.Fragment>
<IconizedContextMenuOptionList>
{ homeButton }
{ dndButton }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell"
label={_t("Notifications")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ feedbackButton }
<IconizedContextMenuOption
className="mx_IconizedContextMenu_option_red"
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
);
if (MatrixClientPeg.get().isGuest()) {
primaryOptionList = (
<IconizedContextMenuOptionList>
{ homeButton }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell"
label={_t("Notification settings")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ /* <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconArchive"
label={_t("Archived rooms")}
onClick={this.onShowArchived}
/> */ }
{ feedbackButton }
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
</React.Fragment>
);
let secondarySection = null;
if (prototypeCommunityName) {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
primaryHeader = (
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ prototypeCommunityName }
</span>
</div>
);
let settingsOption;
let inviteOption;
if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
inviteOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconInvite"
label={_t("Invite")}
onClick={this.onCommunityInviteClick}
/>
);
}
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={this.onCommunitySettingsClick}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
);
}
primaryOptionList = (
<IconizedContextMenuOptionList>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={this.onCommunityMembersClick}
/>
{ inviteOption }
{ feedbackButton }
</IconizedContextMenuOptionList>
);
secondarySection = (
<React.Fragment>
<hr />
<div className="mx_UserMenu_contextMenu_header">
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ OwnProfileStore.instance.displayName }
</span>
<span className="mx_UserMenu_contextMenu_userId">
{ MatrixClientPeg.get().getUserId() }
</span>
</div>
</div>
<IconizedContextMenuOptionList>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("User settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ feedbackButton }
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
</React.Fragment>
);
} else if (MatrixClientPeg.get().isGuest()) {
primaryOptionList = (
<React.Fragment>
<IconizedContextMenuOptionList>
{ homeButton }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ feedbackButton }
</IconizedContextMenuOptionList>
</React.Fragment>
);
}
const classes = classNames({
"mx_UserMenu_contextMenu": true,
"mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName,
});
const position = this.props.isPanelCollapsed
? toRightOf(this.state.contextMenuPosition)
: below(this.state.contextMenuPosition);
return <IconizedContextMenu
// numerical adjustments to overlap the context menu by just over the width of the
// menu icon and make it look connected
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 10}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height + 8}
{...position}
onFinished={this.onCloseMenu}
className={classes}
className="mx_UserMenu_contextMenu"
>
<div className="mx_UserMenu_contextMenu_header">
{ primaryHeader }
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ OwnProfileStore.instance.displayName }
</span>
<span className="mx_UserMenu_contextMenu_userId">
{ MatrixClientPeg.get().getUserId() }
</span>
</div>
<AccessibleTooltipButton
className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
@ -582,9 +498,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
</AccessibleTooltipButton>
</div>
{ customStatusSection }
{ topSection }
{ primaryOptionList }
{ secondarySection }
</IconizedContextMenu>;
};
@ -595,102 +511,45 @@ export default class UserMenu extends React.Component<IProps, IState> {
const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
let isPrototype = false;
let menuName = _t("User menu");
let name = <span className="mx_UserMenu_userName">{ displayName }</span>;
let buttons = (
<span className="mx_UserMenu_headerButtons">
{ /* masked image in CSS */ }
</span>
);
let dnd;
if (this.state.selectedSpace) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{ displayName }</span>
<RoomName room={this.state.selectedSpace}>
{ (roomName) => <span className="mx_UserMenu_subUserName">{ roomName }</span> }
</RoomName>
</div>
);
} else if (prototypeCommunityName) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{ prototypeCommunityName }</span>
<span className="mx_UserMenu_subUserName">{ displayName }</span>
</div>
);
menuName = _t("Community and user menu");
isPrototype = true;
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{ _t("Home") }</span>
<span className="mx_UserMenu_subUserName">{ displayName }</span>
</div>
);
isPrototype = true;
} else if (SettingsStore.getValue("feature_dnd")) {
const isDnd = SettingsStore.getValue("doNotDisturb");
dnd = <AccessibleButton
onClick={this.onDndToggle}
className={classNames({
"mx_UserMenu_dnd": true,
"mx_UserMenu_dnd_noisy": !isDnd,
"mx_UserMenu_dnd_muted": isDnd,
})}
/>;
}
if (this.props.isMinimized) {
name = null;
buttons = null;
let badge: JSX.Element;
if (this.state.dndEnabled) {
badge = <div className="mx_UserMenu_dndBadge" />;
}
const classes = classNames({
'mx_UserMenu': true,
'mx_UserMenu_minimized': this.props.isMinimized,
'mx_UserMenu_prototype': isPrototype,
});
let name: JSX.Element;
if (!this.props.isPanelCollapsed) {
name = <div className="mx_UserMenu_name">
{ displayName }
</div>;
}
return (
<React.Fragment>
<ContextMenuButton
className={classes}
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={menuName}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
>
<div className="mx_UserMenu_row">
<span className="mx_UserMenu_userAvatarContainer">
<BaseAvatar
idName={userId}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_UserMenu_userAvatar"
/>
</span>
{ name }
{ this.state.pendingRoomJoin.size > 0 && (
<InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: this.state.pendingRoomJoin.size },
)} />
</InlineSpinner>
) }
{ dnd }
{ buttons }
</div>
</ContextMenuButton>
<ContextMenuButton
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("User menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
className={classNames("mx_UserMenu", {
mx_UserMenu_cutout: badge,
})}
>
<div className="mx_UserMenu_userAvatar">
<BaseAvatar
idName={userId}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_UserMenu_userAvatar_BaseAvatar"
/>
{ badge }
</div>
{ name }
{ this.renderContextMenu() }
</React.Fragment>
</ContextMenuButton>
);
}
}

View File

@ -1,159 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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, { createRef } from 'react';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames';
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import ContextMenu, { ChevronFace, ContextMenuButton } from "../../structures/ContextMenu";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
interface IProps {
member: RoomMember;
width?: number;
height?: number;
resizeMethod?: ResizeMethod;
}
interface IState {
hasStatus: boolean;
menuDisplayed: boolean;
}
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
public static defaultProps: Partial<IProps> = {
width: 40,
height: 40,
resizeMethod: 'crop',
};
private button = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
this.state = {
hasStatus: this.hasStatus,
menuDisplayed: false,
};
}
public componentDidMount(): void {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}
if (!SettingsStore.getValue("feature_custom_status")) {
return;
}
const { user } = this.props.member;
if (!user) {
return;
}
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
}
public componentWillUnmount(): void {
const { user } = this.props.member;
if (!user) {
return;
}
user.removeListener(
"User._unstable_statusMessage",
this.onStatusMessageCommitted,
);
}
private get hasStatus(): boolean {
const { user } = this.props.member;
if (!user) {
return false;
}
return !!user.unstable_statusMessage;
}
private onStatusMessageCommitted = (): void => {
// The `User` object has observed a status message change.
this.setState({
hasStatus: this.hasStatus,
});
};
private openMenu = (): void => {
this.setState({ menuDisplayed: true });
};
private closeMenu = (): void => {
this.setState({ menuDisplayed: false });
};
public render(): JSX.Element {
const avatar = <MemberAvatar
member={this.props.member}
width={this.props.width}
height={this.props.height}
resizeMethod={this.props.resizeMethod}
/>;
if (!SettingsStore.getValue("feature_custom_status")) {
return avatar;
}
const classes = classNames({
"mx_MemberStatusMessageAvatar": true,
"mx_MemberStatusMessageAvatar_hasStatus": this.state.hasStatus,
});
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this.button.current.getBoundingClientRect();
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
const chevronMargin = 1; // Add some spacing away from target
contextMenu = (
<ContextMenu
chevronOffset={(elementRect.width - chevronWidth) / 2}
chevronFace={ChevronFace.Bottom}
left={elementRect.left + window.pageXOffset}
top={elementRect.top + window.pageYOffset - chevronMargin}
menuWidth={226}
onFinished={this.closeMenu}
>
<StatusMessageContextMenu user={this.props.member.user} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuButton
className={classes}
inputRef={this.button}
onClick={this.openMenu}
isExpanded={this.state.menuDisplayed}
label={_t("User Status")}
>
{ avatar }
</ContextMenuButton>
{ contextMenu }
</React.Fragment>;
}
}

View File

@ -23,6 +23,7 @@ import ContextMenu, {
MenuItem,
MenuItemCheckbox, MenuItemRadio,
} from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler";
interface IProps extends IContextMenuProps {
className?: string;
@ -41,6 +42,7 @@ interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
iconClassName: string;
words?: boolean;
}
interface IRadioProps extends React.ComponentProps<typeof MenuItemRadio> {
@ -73,8 +75,21 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
iconClassName,
active,
className,
words,
...props
}) => {
let marker: JSX.Element;
if (words) {
marker = <span className="mx_IconizedContextMenu_activeText">
{ active ? _t("On") : _t("Off") }
</span>;
} else {
marker = <span className={classNames("mx_IconizedContextMenu_icon", {
mx_IconizedContextMenu_checked: active,
mx_IconizedContextMenu_unchecked: !active,
})} />;
}
return <MenuItemCheckbox
{...props}
className={classNames(className, {
@ -85,10 +100,7 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{ label }</span>
<span className={classNames("mx_IconizedContextMenu_icon", {
mx_IconizedContextMenu_checked: active,
mx_IconizedContextMenu_unchecked: !active,
})} />
{ marker }
</MenuItemCheckbox>;
};

View File

@ -26,7 +26,6 @@ import { _t } from "../../../languageHandler";
import {
leaveSpace,
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showCreateNewSubspace,
showSpaceInvite,
@ -35,18 +34,16 @@ import {
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomViewStore from "../../../stores/RoomViewStore";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { BetaPill } from "../beta/BetaCard";
import SettingsStore from "../../../settings/SettingsStore";
import { Action } from "../../../dispatcher/actions";
interface IProps extends IContextMenuProps {
space: Room;
hideHeader?: boolean;
}
const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) => {
const cli = useContext(MatrixClientContext);
const userId = cli.getUserId();
@ -64,14 +61,14 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
<IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton"
iconClassName="mx_SpacePanel_iconInvite"
label={_t("Invite people")}
label={_t("Invite")}
onClick={onInviteClick}
/>
);
}
let settingsOption;
let leaveSection;
let leaveOption;
if (shouldShowSpaceSettings(space)) {
const onSettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
@ -97,16 +94,17 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished();
};
leaveSection = <IconizedContextMenuOptionList red first>
leaveOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconLeave"
className="mx_IconizedContextMenu_option_red"
label={_t("Leave space")}
onClick={onLeaveClick}
/>
</IconizedContextMenuOptionList>;
);
}
let devtoolsSection;
let devtoolsOption;
if (SettingsStore.getValue("developerMode")) {
const onViewTimelineClick = (ev: ButtonEvent) => {
ev.preventDefault();
@ -120,13 +118,13 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished();
};
devtoolsSection = <IconizedContextMenuOptionList first>
devtoolsOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconSettings"
label={_t("See room timeline (devtools)")}
onClick={onViewTimelineClick}
/>
</IconizedContextMenuOptionList>;
);
}
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
@ -141,14 +139,6 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished();
};
const onAddExistingRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(space);
onFinished();
};
const onNewSubspaceClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -157,46 +147,25 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished();
};
newRoomSection = <IconizedContextMenuOptionList first>
newRoomSection = <>
<div className="mx_SpacePanel_contextMenu_separatorLabel">
{ _t("Add") }
</div>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Create new room")}
label={_t("Room")}
onClick={onNewRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHash"
label={_t("Add existing room")}
onClick={onAddExistingRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Add space")}
label={_t("Space")}
onClick={onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>;
</>;
}
const onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
if (!RoomViewStore.getRoomId()) {
defaultDispatcher.dispatch({
action: "view_room",
room_id: space.roomId,
}, true);
}
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.SpaceMemberList,
refireParams: { space },
});
onFinished();
};
const onExploreRoomsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -214,26 +183,21 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
className="mx_SpacePanel_contextMenu"
compact
>
<div className="mx_SpacePanel_contextMenu_header">
{ !hideHeader && <div className="mx_SpacePanel_contextMenu_header">
{ space.name }
</div>
</div> }
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")}
onClick={onMembersClick}
/>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
onClick={onExploreRoomsClick}
/>
{ settingsOption }
{ leaveOption }
{ devtoolsOption }
{ newRoomSection }
</IconizedContextMenuOptionList>
{ newRoomSection }
{ leaveSection }
{ devtoolsSection }
</IconizedContextMenu>;
};

View File

@ -1,155 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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, { ChangeEvent } from 'react';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { User } from "matrix-js-sdk/src/models/user";
import Spinner from "../elements/Spinner";
interface IProps {
// js-sdk User object. Not required because it might not exist.
user?: User;
}
interface IState {
message: string;
waiting: boolean;
}
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
message: this.comittedStatusMessage,
waiting: false,
};
}
public componentDidMount(): void {
const { user } = this.props;
if (!user) {
return;
}
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
}
public componentWillUnmount(): void {
const { user } = this.props;
if (!user) {
return;
}
user.removeListener(
"User._unstable_statusMessage",
this.onStatusMessageCommitted,
);
}
get comittedStatusMessage(): string {
return this.props.user ? this.props.user.unstable_statusMessage : "";
}
private onStatusMessageCommitted = (): void => {
// The `User` object has observed a status message change.
this.setState({
message: this.comittedStatusMessage,
waiting: false,
});
};
private onClearClick = (): void=> {
MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({
waiting: true,
});
};
private onSubmit = (e: ButtonEvent): void => {
e.preventDefault();
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
this.setState({
waiting: true,
});
};
private onStatusChange = (e: ChangeEvent): void => {
// The input field's value was changed.
this.setState({
message: (e.target as HTMLInputElement).value,
});
};
public render(): JSX.Element {
let actionButton;
if (this.comittedStatusMessage) {
if (this.state.message === this.comittedStatusMessage) {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
onClick={this.onClearClick}
>
<span>{ _t("Clear status") }</span>
</AccessibleButton>;
} else {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
onClick={this.onSubmit}
>
<span>{ _t("Update status") }</span>
</AccessibleButton>;
}
} else {
actionButton = <AccessibleButton
className="mx_StatusMessageContextMenu_submit"
disabled={!this.state.message}
onClick={this.onSubmit}
>
<span>{ _t("Set status") }</span>
</AccessibleButton>;
}
let spinner = null;
if (this.state.waiting) {
spinner = <Spinner w={24} h={24} />;
}
const form = <form
className="mx_StatusMessageContextMenu_form"
autoComplete="off"
onSubmit={this.onSubmit}
>
<input
type="text"
className="mx_StatusMessageContextMenu_message"
key="message"
placeholder={_t("Set a new status...")}
autoFocus={true}
maxLength={60}
value={this.state.message}
onChange={this.onStatusChange}
/>
<div className="mx_StatusMessageContextMenu_actionContainer">
{ actionButton }
{ spinner }
</div>
</form>;
return <div className="mx_StatusMessageContextMenu">
{ form }
</div>;
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
@ -26,6 +26,9 @@ import BugReportDialog from "./BugReportDialog";
import InfoDialog from "./InfoDialog";
import StyledRadioGroup from "../elements/StyledRadioGroup";
import { IDialogProps } from "./IDialogProps";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import { useStateToggle } from "../../../hooks/useStateToggle";
import StyledCheckbox from "../elements/StyledCheckbox";
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
@ -34,18 +37,33 @@ const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose"
interface IProps extends IDialogProps {}
const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
const feedbackRef = useRef<Field>();
const [rating, setRating] = useState<Rating>();
const [comment, setComment] = useState<string>("");
const [canContact, toggleCanContact] = useStateToggle(false);
useEffect(() => {
// autofocus doesn't work on textareas
feedbackRef.current?.focus();
}, []);
const onDebugLogsLinkClick = (): void => {
props.onFinished();
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
const hasFeedback = CountlyAnalytics.instance.canEnable();
const countlyEnabled = CountlyAnalytics.instance.canEnable();
const rageshakeUrl = SdkConfig.get().bug_report_endpoint_url;
const hasFeedback = countlyEnabled || rageshakeUrl;
const onFinished = (sendFeedback: boolean): void => {
if (hasFeedback && sendFeedback) {
CountlyAnalytics.instance.reportFeedback(rating, comment);
if (rageshakeUrl) {
submitFeedback(rageshakeUrl, "feedback", comment, canContact);
} else if (countlyEnabled) {
CountlyAnalytics.instance.reportFeedback(rating, comment);
}
Modal.createTrackedDialog('Feedback sent', '', InfoDialog, {
title: _t('Feedback sent'),
description: _t('Thank you!'),
@ -56,56 +74,73 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
const brand = SdkConfig.get().brand;
let countlyFeedbackSection;
if (hasFeedback) {
countlyFeedbackSection = <React.Fragment>
<hr />
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
<h3>{ _t("Rate %(brand)s", { brand }) }</h3>
let feedbackSection;
if (rageshakeUrl) {
feedbackSection = <div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
<h3>{ _t("Comment") }</h3>
<p>{ _t("Tell us below how you feel about %(brand)s so far.", { brand }) }</p>
<p>{ _t("Please go into as much detail as you like, so we can track down the problem.") }</p>
<p>{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }</p>
<StyledRadioGroup
name="feedbackRating"
value={String(rating)}
onChange={(r) => setRating(parseInt(r, 10) as Rating)}
definitions={[
{ value: "1", label: "😠" },
{ value: "2", label: "😞" },
{ value: "3", label: "😑" },
{ value: "4", label: "😄" },
{ value: "5", label: "😍" },
]}
/>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
ref={feedbackRef}
/>
<Field
id="feedbackComment"
label={_t("Add comment")}
placeholder={_t("Comment")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
/>
</div>
</React.Fragment>;
}
<StyledCheckbox
checked={canContact}
onChange={toggleCanContact}
>
{ _t("You may contact me if you want to follow up or to let me test out upcoming ideas") }
</StyledCheckbox>
</div>;
} else if (countlyEnabled) {
feedbackSection = <div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
<h3>{ _t("Rate %(brand)s", { brand }) }</h3>
let subheading;
if (hasFeedback) {
subheading = (
<h2>{ _t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand }) }</h2>
);
<p>{ _t("Tell us below how you feel about %(brand)s so far.", { brand }) }</p>
<p>{ _t("Please go into as much detail as you like, so we can track down the problem.") }</p>
<StyledRadioGroup
name="feedbackRating"
value={String(rating)}
onChange={(r) => setRating(parseInt(r, 10) as Rating)}
definitions={[
{ value: "1", label: "😠" },
{ value: "2", label: "😞" },
{ value: "3", label: "😑" },
{ value: "4", label: "😄" },
{ value: "5", label: "😍" },
]}
/>
<Field
id="feedbackComment"
label={_t("Add comment")}
placeholder={_t("Comment")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
ref={feedbackRef}
/>
</div>;
}
let bugReports = null;
if (SdkConfig.get().bug_report_endpoint_url) {
if (rageshakeUrl) {
bugReports = (
<p>{
<p className="mx_FeedbackDialog_section_microcopy">{
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
"to help us track down the problem.", {}, {
debugLogsLink: sub => (
@ -121,8 +156,6 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
hasCancelButton={!!hasFeedback}
title={_t("Feedback")}
description={<React.Fragment>
{ subheading }
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug">
<h3>{ _t("Report a bug") }</h3>
<p>{
@ -138,10 +171,10 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
}</p>
{ bugReports }
</div>
{ countlyFeedbackSection }
{ feedbackSection }
</React.Fragment>}
button={hasFeedback ? _t("Send feedback") : _t("Go back")}
buttonDisabled={hasFeedback && !rating}
buttonDisabled={hasFeedback && !rageshakeUrl && !rating}
onFinished={onFinished}
/>);
};

View File

@ -49,7 +49,6 @@ import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInse
import { Action } from "../../../dispatcher/actions";
import EditorModel from "../../../editor/model";
import EmojiPicker from '../emojipicker/EmojiPicker';
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
import Modal from "../../../Modal";
import { RelationType } from 'matrix-js-sdk/src/@types/event';
@ -61,16 +60,6 @@ import PollCreateDialog from "../elements/PollCreateDialog";
let instanceCount = 0;
const NARROW_MODE_BREAKPOINT = 500;
interface IComposerAvatarProps {
me: RoomMember;
}
function ComposerAvatar(props: IComposerAvatarProps) {
return <div className="mx_MessageComposer_avatar">
<MemberStatusMessageAvatar member={props.me} width={24} height={24} />
</div>;
}
interface ISendButtonProps {
onClick: () => void;
title?: string; // defaults to something generic
@ -567,7 +556,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
render() {
const controls = [
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
this.props.e2eStatus ?
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
null,

View File

@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, ReactComponentElement } from "react";
import { Dispatcher } from "flux";
import React, { ComponentType, createRef, ReactComponentElement, RefObject } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event";
@ -27,9 +26,8 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/Roo
import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist from "./RoomSublist";
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
import { ActionPayload } from "../../../dispatcher/payloads";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar";
@ -41,18 +39,28 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { ISuggestedRoom, MetaSpace, SpaceKey, UPDATE_SUGGESTED_ROOMS } from "../../../stores/spaces";
import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import {
ISuggestedRoom,
MetaSpace,
SpaceKey,
UPDATE_SUGGESTED_ROOMS,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
import { shouldShowSpaceInvite, showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
@ -95,9 +103,7 @@ const ALWAYS_VISIBLE_TAGS: TagID[] = [
interface ITagAesthetics {
sectionLabel: string;
sectionLabelRaw?: string;
addRoomLabel?: string;
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
isInvite: boolean;
defaultHidden: boolean;
}
@ -107,6 +113,194 @@ interface ITagAestheticsMap {
[tagId: TagID]: ITagAesthetics;
}
const auxButtonContextMenuPosition = (handle: RefObject<HTMLDivElement>) => {
const rect = handle.current.getBoundingClientRect();
return {
chevronFace: ChevronFace.None,
left: rect.left - 7,
top: rect.top + rect.height,
};
};
const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonProps) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpaceRoom;
});
const showCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
const showInviteUsers = shouldShowComponent(UIComponent.InviteUsers);
if (activeSpace && (showCreateRooms || showInviteUsers)) {
let contextMenu: JSX.Element;
if (menuDisplayed) {
const canInvite = shouldShowSpaceInvite(activeSpace);
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
<IconizedContextMenuOptionList first>
{ showCreateRooms && <IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomList_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_chat" });
}}
/> }
{ showInviteUsers && <IconizedContextMenuOption
label={_t("Invite to space")}
iconClassName="mx_RoomList_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showSpaceInvite(activeSpace);
}}
disabled={!canInvite}
tooltip={canInvite ? undefined
: _t("You do not have permissions to invite people to this space")}
/> }
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
return <>
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={openMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Add people")}
title={_t("Add people")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
{ contextMenu }
</>;
} else if (!activeSpace && showCreateRooms) {
return <AccessibleTooltipButton
tabIndex={tabIndex}
onClick={() => dispatcher.dispatch({ action: 'view_create_chat' })}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Start chat")}
title={_t("Start chat")}
/>;
}
};
const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpaceRoom;
});
const showCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
let contextMenuContent: JSX.Element;
if (menuDisplayed && activeSpace) {
const canAddRooms = activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
MatrixClientPeg.get().getUserId());
contextMenuContent = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({
action: "view_room",
room_id: activeSpace.roomId,
});
}}
/>
{
showCreateRoom
? (<>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconCreateNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewRoom(activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconAddExistingRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showAddExistingRooms(activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
/>
</>)
: null
}
</IconizedContextMenuOptionList>;
} else if (menuDisplayed) {
contextMenuContent = <IconizedContextMenuOptionList first>
{ showCreateRoom && <IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconCreateNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_room" });
}}
/> }
<IconizedContextMenuOption
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
? _t("Explore community rooms")
: _t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
}
let contextMenu: JSX.Element;
if (menuDisplayed) {
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
{ contextMenuContent }
</IconizedContextMenu>;
}
return <>
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={openMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_td("Add room")}
title={_td("Add room")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
{ contextMenu }
</>;
};
const TAG_AESTHETICS: ITagAestheticsMap = {
[DefaultTagID.Invite]: {
sectionLabel: _td("Invites"),
@ -122,93 +316,13 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
sectionLabel: _td("People"),
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Start chat"),
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
(dispatcher || defaultDispatcher).dispatch({ action: 'view_create_chat' });
},
AuxButtonComponent: DmAuxButton,
},
[DefaultTagID.Untagged]: {
sectionLabel: _td("Rooms"),
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Add room"),
addRoomContextMenu: (onFinished: () => void) => {
if (SpaceStore.instance.activeSpaceRoom) {
const userId = MatrixClientPeg.get().getUserId();
const space = SpaceStore.instance.activeSpaceRoom;
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
return <IconizedContextMenuOptionList first>
{
shouldShowComponent(UIComponent.CreateRooms)
? (<>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(space);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(space);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
/>
</>)
: null
}
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconBrowse"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
}
return <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.dispatch({ action: "view_create_room" });
}}
/>
<IconizedContextMenuOption
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
? _t("Explore community rooms")
: _t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
},
AuxButtonComponent: UntaggedAuxButton,
},
[DefaultTagID.LowPriority]: {
sectionLabel: _td("Low priority"),
@ -296,7 +410,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const currentRoomId = RoomViewStore.getRoomId();
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
if (room) {
dis.dispatch({
defaultDispatcher.dispatch({
action: Action.ViewRoom,
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
@ -375,17 +489,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private onStartChat = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
dis.dispatch({ action: "view_create_chat", initialText });
defaultDispatcher.dispatch({ action: "view_create_chat", initialText });
};
private onExplore = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
};
private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
showSpaceInvite(this.context.getRoom(this.props.activeSpace), initialText);
if (this.props.activeSpace[0] === "!") {
defaultDispatcher.dispatch({
action: "view_room",
room_id: SpaceStore.instance.activeSpace,
});
} else {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory, initialText });
}
};
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
@ -508,9 +624,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
forRooms={true}
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
AuxButtonComponent={aesthetics.AuxButtonComponent}
isMinimized={this.props.isMinimized}
showSkeleton={showSkeleton}
extraTiles={extraTiles}
@ -528,10 +642,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}
public render() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const activeSpace = this.props.activeSpace[0] === "!" ? cli.getRoom(this.props.activeSpace) : null;
let explorePrompt: JSX.Element;
if (!this.props.isMinimized) {
if (this.state.isNameFiltering) {
@ -549,58 +659,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
kind="link"
onClick={this.onExplore}
>
{ activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
{ this.props.activeSpace[0] === "!" ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton>
</div>;
} else if (
activeSpace?.canInvite(userId) ||
activeSpace?.getMyMembership() === "join" ||
activeSpace?.getJoinRule() === JoinRule.Public
) {
const spaceName = activeSpace.name;
const canInvite = activeSpace?.canInvite(userId) || activeSpace?.getJoinRule() === JoinRule.Public;
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div>
{ canInvite && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick}
title={_t("Invite to %(spaceName)s", { spaceName })}
>
{ _t("Invite people") }
</AccessibleTooltipButton> }
{ activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore}
title={_t("Explore %(spaceName)s", { spaceName })}
>
{ _t("Explore rooms") }
</AccessibleTooltipButton> }
</div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
const unfilteredLists = RoomListStore.instance.unfilteredLists;
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
<AccessibleButton
className="mx_RoomList_explorePrompt_startChat"
kind="link"
onClick={this.onStartChat}
>
{ _t("Start a new chat") }
</AccessibleButton>
<AccessibleButton
className="mx_RoomList_explorePrompt_explore"
kind="link"
onClick={this.onExplore}
>
{ _t("Explore all public rooms") }
</AccessibleButton>
</div>;
}
}
}

View File

@ -0,0 +1,367 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useContext, useEffect, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import { HomeButtonContextMenu } from "../spaces/SpacePanel";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher";
import { shouldShowSpaceInvite, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { ButtonEvent } from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import EditCommunityPrototypeDialog from "../dialogs/EditCommunityPrototypeDialog";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import ErrorDialog from "../dialogs/ErrorDialog";
import { showCommunityInviteDialog } from "../../../RoomInvite";
import { useDispatcher } from "../../../hooks/useDispatcher";
import InlineSpinner from "../elements/InlineSpinner";
import TooltipButton from "../elements/TooltipButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import {
getMetaSpaceName,
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
const contextMenuBelow = (elementRect: DOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset;
const top = elementRect.bottom + window.pageYOffset + 12;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
const PrototypeCommunityContextMenu = (props) => {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
let settingsOption;
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
const onCommunitySettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
props.onFinished();
};
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={onCommunitySettingsClick}
/>
);
}
const onCommunityMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// We'd ideally just pop open a right panel with the member list, but the current
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
// switch to the general room and open the member list there as it should be in sync
// anyways.
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat) {
dis.dispatch({
action: 'view_room',
room_id: chat.roomId,
}, true);
dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
title: _t('Failed to find the general chat for this community'),
description: _t("Failed to find the general chat for this community"),
});
}
props.onFinished();
};
return <IconizedContextMenu {...props} compact>
<IconizedContextMenuOptionList first>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={onCommunityMembersClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
};
const useJoiningRooms = (): Set<string> => {
const cli = useContext(MatrixClientContext);
const [joiningRooms, setJoiningRooms] = useState(new Set<string>());
useDispatcher(defaultDispatcher, payload => {
switch (payload.action) {
case Action.JoinRoom:
setJoiningRooms(new Set(joiningRooms.add(payload.roomId)));
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
if (joiningRooms.delete(payload.roomId)) {
setJoiningRooms(new Set(joiningRooms));
}
break;
}
});
useEventEmitter(cli, "Room", (room: Room) => {
if (joiningRooms.delete(room.roomId)) {
setJoiningRooms(new Set(joiningRooms));
}
});
return joiningRooms;
};
interface IProps {
onVisibilityChange?(): void;
}
const RoomListHeader = ({ onVisibilityChange }: IProps) => {
const cli = useContext(MatrixClientContext);
const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu<HTMLDivElement>();
const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu<HTMLDivElement>();
const [spaceKey, activeSpace] = useEventEmitterState<[SpaceKey, Room | null]>(
SpaceStore.instance,
UPDATE_SELECTED_SPACE,
() => [SpaceStore.instance.activeSpace, SpaceStore.instance.activeSpaceRoom],
);
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
const joiningRooms = useJoiningRooms();
const count = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
if (RoomListStore.instance.getFirstNameFilterCondition()) {
return Object.values(RoomListStore.instance.orderedLists).flat(1).length;
} else {
return null;
}
});
useEffect(() => {
if (onVisibilityChange) {
onVisibilityChange();
}
}, [count, onVisibilityChange]);
if (typeof count === "number") {
return <div className="mx_LeftPanel_roomListFilterCount">
{ _t("%(count)s results", { count }) }
</div>;
}
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
let contextMenu: JSX.Element;
if (mainMenuDisplayed) {
let ContextMenuComponent;
if (activeSpace) {
ContextMenuComponent = SpaceContextMenu;
} else if (communityId) {
ContextMenuComponent = PrototypeCommunityContextMenu;
} else {
ContextMenuComponent = HomeButtonContextMenu;
}
contextMenu = <ContextMenuComponent
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
space={activeSpace}
onFinished={closeMainMenu}
hideHeader={true}
/>;
} else if (plusMenuDisplayed && activeSpace) {
let inviteOption: JSX.Element;
if (shouldShowSpaceInvite(activeSpace)) {
inviteOption = <IconizedContextMenuOption
label={_t("Invite")}
iconClassName="mx_RoomListHeader_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showSpaceInvite(activeSpace);
closePlusMenu();
}}
/>;
} else if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
inviteOption = <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconInvite"
label={_t("Invite")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
closePlusMenu();
}}
/>;
}
let createNewRoomOption: JSX.Element;
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
createNewRoomOption = <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconCreateRoom"
label={_t("Create new room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(activeSpace);
closePlusMenu();
}}
/>;
}
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
closePlusMenu();
}}
/>
{ createNewRoomOption }
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
closePlusMenu();
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
} else if (plusMenuDisplayed) {
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomListHeader_iconCreateRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_room" });
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("Join public room")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
closePlusMenu();
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
let title: string;
if (activeSpace) {
title = activeSpace.name;
} else if (communityId) {
title = CommunityPrototypeStore.instance.getSelectedCommunityName();
} else {
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
}
let pendingRoomJoinSpinner;
if (joiningRooms.size) {
pendingRoomJoinSpinner = <InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: joiningRooms.size },
)} />
</InlineSpinner>;
}
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{ title }</div>;
if (activeSpace || spaceKey === MetaSpace.Home) {
contextMenuButton = <ContextMenuTooltipButton
inputRef={mainMenuHandle}
onClick={openMainMenu}
isExpanded={mainMenuDisplayed}
className="mx_RoomListHeader_contextMenuButton"
title={activeSpace
? _t("%(spaceName)s menu", { spaceName: activeSpace.name })
: _t("Home options")}
>
{ title }
</ContextMenuTooltipButton>;
}
return <div className="mx_RoomListHeader">
{ contextMenuButton }
{ pendingRoomJoinSpinner }
<ContextMenuTooltipButton
inputRef={plusMenuHandle}
onClick={openPlusMenu}
isExpanded={plusMenuDisplayed}
className="mx_RoomListHeader_plusButton"
title={_t("Add")}
/>
{ contextMenu }
</div>;
};
export default RoomListHeader;

View File

@ -1,55 +0,0 @@
/*
Copyright 2020 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, { useEffect, useState } from "react";
import { _t } from "../../../languageHandler";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/spaces/SpaceStore";
interface IProps {
onVisibilityChange?: () => void;
}
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
const [count, setCount] = useState<number>(null);
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
if (RoomListStore.instance.getFirstNameFilterCondition()) {
const numRooms = Object.values(RoomListStore.instance.orderedLists).flat(1).length;
setCount(numRooms);
} else {
setCount(null);
}
});
useEffect(() => {
if (onVisibilityChange) {
onVisibilityChange();
}
}, [count, onVisibilityChange]);
if (typeof count !== "number") return null;
return <div className="mx_LeftPanel_roomListFilterCount">
{ SpaceStore.instance.spacePanelSpaces.length
? _t("%(count)s results in all spaces", { count })
: _t("%(count)s results", { count })
}
</div>;
};
export default RoomListNumResults;

View File

@ -17,7 +17,7 @@ limitations under the License.
*/
import * as React from "react";
import { createRef, ReactComponentElement } from "react";
import { ComponentType, createRef, ReactComponentElement } from "react";
import { normalize } from "matrix-js-sdk/src/utils";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames';
@ -52,11 +52,9 @@ import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
import ExtraTile from "./ExtraTile";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { Dispatcher } from "flux";
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@ -67,13 +65,16 @@ const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
// HACK: We really shouldn't have to do this.
polyfillTouchEvent();
export interface IAuxButtonProps {
tabIndex: number;
dispatcher?: Dispatcher<ActionPayload>;
}
interface IProps {
forRooms: boolean;
startAsHidden: boolean;
label: string;
onAddRoom?: () => void;
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
addRoomLabel: string;
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
isMinimized: boolean;
tagId: TagID;
showSkeleton?: boolean;
@ -81,8 +82,6 @@ interface IProps {
resizeNotifier: ResizeNotifier;
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
onListCollapse?: (isExpanded: boolean) => void;
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
}
// TODO: Use re-resizer's NumberSize when it is exposed as the type
@ -95,7 +94,6 @@ type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
interface IState {
contextMenuPosition: PartialDOMRect;
addRoomContextMenuPosition: PartialDOMRect;
isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number;
@ -123,7 +121,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
this.state = {
contextMenuPosition: null,
addRoomContextMenuPosition: null,
isResizing: false,
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
@ -313,11 +310,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
};
private onAddRoom = (e) => {
e.stopPropagation();
if (this.props.onAddRoom) this.props.onAddRoom();
};
private applyHeightChange(newHeight: number) {
const heightInTiles = Math.ceil(this.layout.pixelsToTiles(newHeight - this.padding));
this.layout.visibleTiles = Math.min(this.numTiles, heightInTiles);
@ -395,21 +387,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
});
};
private onAddRoomContextMenu = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({ addRoomContextMenuPosition: target.getBoundingClientRect() });
};
private onCloseMenu = () => {
this.setState({ contextMenuPosition: null });
};
private onCloseAddRoomMenu = () => {
this.setState({ addRoomContextMenuPosition: null });
};
private onUnreadFirstChanged = () => {
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
@ -627,18 +608,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
</div>
</ContextMenu>
);
} else if (this.state.addRoomContextMenuPosition) {
contextMenu = (
<IconizedContextMenu
chevronFace={ChevronFace.None}
left={this.state.addRoomContextMenuPosition.left - 7} // center align with the handle
top={this.state.addRoomContextMenuPosition.top + this.state.addRoomContextMenuPosition.height}
onFinished={this.onCloseAddRoomMenu}
compact
>
{ this.props.addRoomContextMenu(this.onCloseAddRoomMenu) }
</IconizedContextMenu>
);
}
return (
@ -677,30 +646,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
);
let addRoomButton = null;
if (!!this.props.onAddRoom && shouldShowComponent(UIComponent.CreateRooms)) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
/>
);
} else if (this.props.addRoomContextMenu) {
// We assume that shouldShowComponent() is checked by the context menu itself.
addRoomButton = (
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoomContextMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
isExpanded={!!this.state.addRoomContextMenuPosition}
/>
);
if (this.props.AuxButtonComponent) {
const AuxButtonComponent = this.props.AuxButtonComponent;
addRoomButton = <AuxButtonComponent tabIndex={tabIndex} />;
}
const collapseClasses = classNames({

View File

@ -36,6 +36,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
getMetaSpaceName,
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
@ -56,6 +57,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import UIStore from "../../../stores/UIStore";
import QuickSettingsButton from "./QuickSettingsButton";
import { useSettingValue } from "../../../hooks/useSettings";
import UserMenu from "../../structures/UserMenu";
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
@ -80,7 +82,11 @@ interface IInnerSpacePanelProps {
setPanelCollapsed: Dispatch<SetStateAction<boolean>>;
}
const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof SpaceContextMenu>) => {
export const HomeButtonContextMenu = ({
onFinished,
hideHeader,
...props
}: ComponentProps<typeof SpaceContextMenu>) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
@ -91,9 +97,9 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof S
className="mx_SpacePanel_contextMenu"
compact
>
<div className="mx_SpacePanel_contextMenu_header">
{ !hideHeader && <div className="mx_SpacePanel_contextMenu_header">
{ _t("Home") }
</div>
</div> }
<IconizedContextMenuOptionList first>
<IconizedContextMenuCheckbox
iconClassName="mx_SpacePanel_noIcon"
@ -135,7 +141,7 @@ const HomeButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
className="mx_SpaceButton_home"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={allRoomsInHome ? _t("All rooms") : _t("Home")}
label={getMetaSpaceName(MetaSpace.Home, allRoomsInHome)}
notificationState={allRoomsInHome
? RoomNotificationStateStore.instance.globalState
: SpaceStore.instance.getNotificationState(MetaSpace.Home)}
@ -150,7 +156,7 @@ const FavouritesButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps)
className="mx_SpaceButton_favourites"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("Favourites")}
label={getMetaSpaceName(MetaSpace.Favourites)}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Favourites)}
/>;
};
@ -161,7 +167,7 @@ const PeopleButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
className="mx_SpaceButton_people"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("People")}
label={getMetaSpaceName(MetaSpace.People)}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.People)}
/>;
};
@ -172,7 +178,7 @@ const OrphansButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) =>
className="mx_SpaceButton_orphans"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("Other rooms")}
label={getMetaSpaceName(MetaSpace.Orphans)}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Orphans)}
/>;
};
@ -246,6 +252,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
});
return <div className="mx_SpaceTreeLevel">
<UserMenu isPanelCollapsed={isPanelCollapsed} />
{ metaSpacesSection }
{ invites.map(s => (
<SpaceItem

View File

@ -798,6 +798,11 @@
"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.",
"Failed to join room": "Failed to join room",
"All rooms": "All rooms",
"Home": "Home",
"Favourites": "Favourites",
"People": "People",
"Other rooms": "Other rooms",
"You joined the call": "You joined the call",
"%(senderName)s joined the call": "%(senderName)s joined the call",
"Call in progress": "Call in progress",
@ -1048,8 +1053,6 @@
"Quick settings": "Quick settings",
"All settings": "All settings",
"Pin to sidebar": "Pin to sidebar",
"Favourites": "Favourites",
"People": "People",
"More options": "More options",
"Theme": "Theme",
"Space selection": "Space selection",
@ -1086,11 +1089,8 @@
"You can change these anytime.": "You can change these anytime.",
"Creating...": "Creating...",
"Create": "Create",
"Home": "Home",
"Show all rooms": "Show all rooms",
"All rooms": "All rooms",
"Options": "Options",
"Other rooms": "Other rooms",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
"Collapse": "Collapse",
@ -1711,16 +1711,20 @@
"Hide Widgets": "Hide Widgets",
"Show Widgets": "Show Widgets",
"Search": "Search",
"Invites": "Invites",
"Start new chat": "Start new chat",
"Invite to space": "Invite to space",
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",
"Add people": "Add people",
"Start chat": "Start chat",
"Add room": "Add room",
"Explore rooms": "Explore rooms",
"Create new room": "Create new room",
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
"Add existing room": "Add existing room",
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
"Explore rooms": "Explore rooms",
"Explore community rooms": "Explore community rooms",
"Explore public rooms": "Explore public rooms",
"Add room": "Add room",
"Invites": "Invites",
"Low priority": "Low priority",
"System Alerts": "System Alerts",
"Historical": "Historical",
@ -1730,13 +1734,17 @@
"Can't see what you're looking for?": "Can't see what you're looking for?",
"Start a new chat": "Start a new chat",
"Explore all public rooms": "Explore all public rooms",
"Quick actions": "Quick actions",
"Explore %(spaceName)s": "Explore %(spaceName)s",
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
"%(count)s results in all spaces|other": "%(count)s results in all spaces",
"%(count)s results in all spaces|one": "%(count)s result in all spaces",
"Community settings": "Community settings",
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
"Members": "Members",
"%(count)s results|other": "%(count)s results",
"%(count)s results|one": "%(count)s result",
"Invite": "Invite",
"Join public room": "Join public room",
"Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms",
"Currently joining %(count)s rooms|one": "Currently joining %(count)s room",
"%(spaceName)s menu": "%(spaceName)s menu",
"Home options": "Home options",
"This room": "This room",
"Joining space …": "Joining space …",
"Joining room …": "Joining room …",
@ -1882,7 +1890,6 @@
"The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to",
"Yours, or the other users' internet connection": "Yours, or the other users' internet connection",
"Yours, or the other users' session": "Yours, or the other users' session",
"Members": "Members",
"Nothing pinned, yet": "Nothing pinned, yet",
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
"Pinned messages": "Pinned messages",
@ -1913,7 +1920,6 @@
"Hide sessions": "Hide sessions",
"Jump to read receipt": "Jump to read receipt",
"Mention": "Mention",
"Invite": "Invite",
"Share Link to User": "Share Link to User",
"Message": "Message",
"Demote yourself?": "Demote yourself?",
@ -2454,14 +2460,15 @@
"Include Attachments": "Include Attachments",
"Export": "Export",
"Feedback sent": "Feedback sent",
"Comment": "Comment",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"Feedback": "Feedback",
"You may contact me if you want to follow up or to let me test out upcoming ideas": "You may contact me if you want to follow up or to let me test out upcoming ideas",
"Rate %(brand)s": "Rate %(brand)s",
"Tell us below how you feel about %(brand)s so far.": "Tell us below how you feel about %(brand)s so far.",
"Please go into as much detail as you like, so we can track down the problem.": "Please go into as much detail as you like, so we can track down the problem.",
"Add comment": "Add comment",
"Comment": "Comment",
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Feedback": "Feedback",
"Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"Send feedback": "Send feedback",
@ -2474,7 +2481,6 @@
"Search for rooms or people": "Search for rooms or people",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
@ -2772,12 +2778,9 @@
"Mentions only": "Mentions only",
"Copy link": "Copy link",
"See room timeline (devtools)": "See room timeline (devtools)",
"Add space": "Add space",
"Room": "Room",
"Space": "Space",
"Manage & explore rooms": "Manage & explore rooms",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
"Set a new status...": "Set a new status...",
"Move up": "Move up",
"Move down": "Move down",
"View Community": "View Community",
@ -2802,7 +2805,6 @@
"Avatar": "Avatar",
"This room is public": "This room is public",
"Away": "Away",
"User Status": "User Status",
"powered by Matrix": "powered by Matrix",
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
"Country Dropdown": "Country Dropdown",
@ -2929,6 +2931,12 @@
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
"Unable to copy room link": "Unable to copy room link",
"Unable to copy a link to the room to the clipboard.": "Unable to copy a link to the room to the clipboard.",
"Testing small changes": "Testing small changes",
"Your feedback is wanted as we try out some design changes.": "Your feedback is wanted as we try out some design changes.",
"More info": "More info",
"We're testing some design changes": "We're testing some design changes",
"Your ongoing feedback would be very welcome, so if you see anything different you want to comment on, <a>please let us know about it</a>. Click your avatar to find a quick feedback link.": "Your ongoing feedback would be very welcome, so if you see anything different you want to comment on, <a>please let us know about it</a>. Click your avatar to find a quick feedback link.",
"If you'd like to preview or test some potential upcoming changes, there's an option in feedback to let us contact you.": "If you'd like to preview or test some potential upcoming changes, there's an option in feedback to let us contact you.",
"Signed Out": "Signed Out",
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
"Terms and Conditions": "Terms and Conditions",
@ -2974,7 +2982,6 @@
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
"Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s",
"Filter": "Filter",
"Filter all spaces": "Filter all spaces",
"Clear filter": "Clear filter",
"Filter rooms and people": "Filter rooms and people",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
@ -2992,7 +2999,6 @@
"Search failed": "Search failed",
"Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(",
"No more results": "No more results",
"Room": "Room",
"Failed to reject invite": "Failed to reject invite",
"Drop file here to upload": "Drop file here to upload",
"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.",
@ -3012,7 +3018,6 @@
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Results": "Results",
"Rooms and spaces": "Rooms and spaces",
"Space": "Space",
"Search names and descriptions": "Search names and descriptions",
"Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",
@ -3020,6 +3025,7 @@
"To join this Space, hide communities in your <a>preferences</a>": "To join this Space, hide communities in your <a>preferences</a>",
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
"Created from <Community />": "Created from <Community />",
"Add space": "Add space",
"Welcome to <name/>": "Welcome to <name/>",
"Random": "Random",
"Support": "Support",
@ -3066,20 +3072,19 @@
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
"Your status will be shown to people you have a DM with.": "Your status will be shown to people you have a DM with.",
"Set status": "Set status",
"Clear status": "Clear status",
"Set a new status": "Set a new status",
"Clear": "Clear",
"Got an account? <a>Sign in</a>": "Got an account? <a>Sign in</a>",
"New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
"Notification settings": "Notification settings",
"Do not disturb": "Do not disturb",
"Security & privacy": "Security & privacy",
"Community settings": "Community settings",
"User settings": "User settings",
"Switch to light mode": "Switch to light mode",
"Switch to dark mode": "Switch to dark mode",
"Switch theme": "Switch theme",
"User menu": "User menu",
"Community and user menu": "Community and user menu",
"Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms",
"Currently joining %(count)s rooms|one": "Currently joining %(count)s room",
"Could not load user profile": "Could not load user profile",
"Decrypted event source": "Decrypted event source",
"Original event source": "Original event source",

View File

@ -288,7 +288,7 @@ export async function submitFeedback(
body.append("user_id", MatrixClientPeg.get()?.getUserId());
for (const k in extraData) {
body.append(k, extraData[k]);
body.append(k, JSON.stringify(extraData[k]));
}
await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});

View File

@ -319,6 +319,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"doNotDisturb": {
supportedLevels: [SettingLevel.DEVICE],
default: false,
controller: new IncompatibleController("feature_dnd", false, false),
},
"mjolnirRooms": {
supportedLevels: [SettingLevel.ACCOUNT],

View File

@ -17,6 +17,8 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room";
import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { _t } from "../../languageHandler";
// The consts & types are moved out here to prevent cyclical imports
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
@ -33,6 +35,19 @@ export enum MetaSpace {
Orphans = "orphans-space",
}
export const getMetaSpaceName = (spaceKey: MetaSpace, allRoomsInHome = false): string => {
switch (spaceKey) {
case MetaSpace.Home:
return allRoomsInHome ? _t("All rooms") : _t("Home");
case MetaSpace.Favourites:
return _t("Favourites");
case MetaSpace.People:
return _t("People");
case MetaSpace.Orphans:
return _t("Other rooms");
}
};
export type SpaceKey = MetaSpace | Room["roomId"];
export interface ISuggestedRoom extends IHierarchyRoom {

View File

@ -18,6 +18,7 @@ import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { calculateRoomVia } from "./permalinks/Permalinks";
import Modal from "../Modal";
@ -101,6 +102,10 @@ export const showCreateNewRoom = async (space: Room): Promise<boolean> => {
return shouldCreate;
};
export const shouldShowSpaceInvite = (space: Room) =>
(space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId())) ||
space.getJoinRule() === JoinRule.Public;
export const showSpaceInvite = (space: Room, initialText = ""): void => {
if (space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {

View File

@ -26,6 +26,7 @@ module.exports = async function toastScenarios(alice, bob) {
alice.log.step(`accepts analytics toast`);
await acceptToast(alice, "Help us improve Element");
await rejectToast(alice, "Testing small changes");
alice.log.done();
alice.log.step(`checks no remaining toasts`);
@ -40,6 +41,7 @@ module.exports = async function toastScenarios(alice, bob) {
bob.log.step(`reject analytics toast`);
await rejectToast(bob, "Help us improve Element");
await rejectToast(bob, "Testing small changes");
bob.log.done();
bob.log.step(`checks no remaining toasts`);

View File

@ -58,7 +58,7 @@ async function inviteSpace(session, spaceName, userId) {
button: 'right',
});
const inviteButton = await session.query('[aria-label="Invite people"]');
const inviteButton = await session.query('.mx_SpacePanel_contextMenu_inviteButton[aria-label="Invite"]');
await inviteButton.click();
try {