Merge branch 'develop' into travis/badge-ts
						commit
						33e4ecbe86
					
				|  | @ -12,6 +12,7 @@ | |||
| @import "./structures/_HeaderButtons.scss"; | ||||
| @import "./structures/_HomePage.scss"; | ||||
| @import "./structures/_LeftPanel.scss"; | ||||
| @import "./structures/_LeftPanel2.scss"; | ||||
| @import "./structures/_MainSplit.scss"; | ||||
| @import "./structures/_MatrixChat.scss"; | ||||
| @import "./structures/_MyGroups.scss"; | ||||
|  | @ -28,6 +29,7 @@ | |||
| @import "./structures/_ToastContainer.scss"; | ||||
| @import "./structures/_TopLeftMenuButton.scss"; | ||||
| @import "./structures/_UploadBar.scss"; | ||||
| @import "./structures/_UserMenuButton.scss"; | ||||
| @import "./structures/_ViewSource.scss"; | ||||
| @import "./structures/auth/_CompleteSecurity.scss"; | ||||
| @import "./structures/auth/_Login.scss"; | ||||
|  | @ -177,10 +179,12 @@ | |||
| @import "./views/rooms/_RoomDropTarget.scss"; | ||||
| @import "./views/rooms/_RoomHeader.scss"; | ||||
| @import "./views/rooms/_RoomList.scss"; | ||||
| @import "./views/rooms/_RoomList2.scss"; | ||||
| @import "./views/rooms/_RoomPreviewBar.scss"; | ||||
| @import "./views/rooms/_RoomRecoveryReminder.scss"; | ||||
| @import "./views/rooms/_RoomSublist2.scss"; | ||||
| @import "./views/rooms/_RoomTile.scss"; | ||||
| @import "./views/rooms/_RoomTile2.scss"; | ||||
| @import "./views/rooms/_RoomUpgradeWarningBar.scss"; | ||||
| @import "./views/rooms/_SearchBar.scss"; | ||||
| @import "./views/rooms/_SendMessageComposer.scss"; | ||||
|  |  | |||
|  | @ -23,14 +23,6 @@ limitations under the License. | |||
|     flex: 0 0 auto; | ||||
| } | ||||
| 
 | ||||
| // TODO: Remove temporary indicator of new room list implementation. | ||||
| // This border is meant to visually distinguish between the two components when the | ||||
| // user has turned on the new room list implementation, at least until the designs | ||||
| // themselves give it away. | ||||
| .mx_LeftPanel2 .mx_LeftPanel { | ||||
|     border-left: 5px #e26dff solid; | ||||
| } | ||||
| 
 | ||||
| .mx_LeftPanel_container.collapsed { | ||||
|     min-width: unset; | ||||
|     /* Collapsed LeftPanel 50px */ | ||||
|  |  | |||
|  | @ -0,0 +1,99 @@ | |||
| /* | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| // TODO: Rename to mx_LeftPanel during replacement of old component | ||||
| 
 | ||||
| // TODO: Put these variables in the right place, or namespace them. | ||||
| $tagPanelWidth: 70px; | ||||
| $roomListMinimizedWidth: 50px; | ||||
| 
 | ||||
| .mx_LeftPanel2 { | ||||
|     background-color: $header-panel-bg-color; | ||||
|     min-width: 260px; | ||||
|     max-width: 50%; | ||||
| 
 | ||||
|     // Create a row-based flexbox for the TagPanel and the room list | ||||
|     display: flex; | ||||
| 
 | ||||
|     .mx_LeftPanel2_tagPanelContainer { | ||||
|         flex-grow: 0; | ||||
|         flex-shrink: 0; | ||||
|         flex-basis: $tagPanelWidth; | ||||
|         height: 100%; | ||||
| 
 | ||||
|         // Create another flexbox so the TagPanel fills the container | ||||
|         display: flex; | ||||
| 
 | ||||
|         // TagPanel handles its own CSS | ||||
|     } | ||||
| 
 | ||||
|     // Note: The 'room list' in this context is actually everything that isn't the tag | ||||
|     // panel, such as the menu options, breadcrumbs, filtering, etc | ||||
|     .mx_LeftPanel2_roomListContainer { | ||||
|         width: calc(100% - $tagPanelWidth); | ||||
| 
 | ||||
|         // Create another flexbox (this time a column) for the room list components | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
| 
 | ||||
|         .mx_LeftPanel2_userHeader { | ||||
|             padding: 14px 12px 20px; // 14px top, 12px sides, 20px bottom | ||||
| 
 | ||||
|             // Create another flexbox column for the rows to stack within | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
| 
 | ||||
|             // There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs | ||||
|             .mx_LeftPanel2_headerRow { | ||||
|                 // Create yet another flexbox, this time within the row, to ensure items stay | ||||
|                 // aligned correctly. This is also a row-based flexbox. | ||||
|                 display: flex; | ||||
|                 align-items: center; | ||||
|             } | ||||
| 
 | ||||
|             .mx_LeftPanel2_userAvatarContainer { | ||||
|                 position: relative; // to make default avatars work | ||||
|                 margin-right: 8px; | ||||
|             } | ||||
| 
 | ||||
|             .mx_LeftPanel2_userName { | ||||
|                 font-weight: 600; | ||||
|                 font-size: $font-15px; | ||||
|                 line-height: $font-20px; | ||||
|                 flex: 1; | ||||
|             } | ||||
| 
 | ||||
|             .mx_LeftPanel2_headerButtons { | ||||
|                 // No special styles: the rest of the layout happens to make it work. | ||||
|             } | ||||
| 
 | ||||
|             .mx_LeftPanel2_breadcrumbsContainer { | ||||
|                 // TODO: Improve CSS for breadcrumbs (currently shoved into the view rather than placed) | ||||
|                 width: 100%; | ||||
|                 overflow: hidden; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .mx_LeftPanel2_filterContainer { | ||||
|             // TODO: Improve CSS for filtering and its input | ||||
|         } | ||||
| 
 | ||||
|         .mx_LeftPanel2_actualRoomListContainer { | ||||
|             flex-grow: 1; // fill the available space | ||||
|             overflow-y: auto; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -66,7 +66,7 @@ limitations under the License. | |||
| } | ||||
| 
 | ||||
| /* not the left panel, and not the resize handle, so the roomview/groupview/... */ | ||||
| .mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) { | ||||
| .mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_LeftPanel2):not(.mx_ResizeHandle) { | ||||
|     background-color: $primary-bg-color; | ||||
| 
 | ||||
|     flex: 1 1 0; | ||||
|  |  | |||
|  | @ -0,0 +1,162 @@ | |||
| /* | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| .mx_UserMenuButton { | ||||
|     // No special styles on the button itself | ||||
| } | ||||
| 
 | ||||
| .mx_UserMenuButton_contextMenu { | ||||
|     width: 231px; | ||||
| 
 | ||||
|     // Put 20px of padding around the whole menu. We do this instead of a | ||||
|     // simple `padding: 20px` rule so the horizontal rules added by the | ||||
|     // optionLists is rendered correctly (full width). | ||||
|     > * { | ||||
|         padding-left: 20px; | ||||
|         padding-right: 20px; | ||||
| 
 | ||||
|         &:first-child { | ||||
|             padding-top: 20px; | ||||
|         } | ||||
| 
 | ||||
|         &:last-child { | ||||
|             padding-bottom: 20px; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_UserMenuButton_contextMenu_header { | ||||
|         // Create a flexbox to organize the header a bit easier | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
| 
 | ||||
|         &:nth-child(n + 1) { | ||||
|             // The first header will have appropriate padding, subsequent ones need a margin. | ||||
|             margin-top: 10px; | ||||
|         } | ||||
| 
 | ||||
|         .mx_UserMenuButton_contextMenu_name { | ||||
|             // Create another flexbox of columns to handle large user IDs | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
| 
 | ||||
|             // fit the container | ||||
|             flex: 1; | ||||
|             width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button | ||||
| 
 | ||||
|             * { | ||||
|                 // Automatically grow all subelements to fit the container | ||||
|                 flex: 1; | ||||
|                 width: 100%; | ||||
| 
 | ||||
|                 // Ellipsize any text overflow | ||||
|                 text-overflow: ellipsis; | ||||
|                 overflow: hidden; | ||||
|                 white-space: nowrap; | ||||
|             } | ||||
| 
 | ||||
|             .mx_UserMenuButton_contextMenu_displayName { | ||||
|                 font-weight: bold; | ||||
|                 font-size: $font-15px; | ||||
|                 line-height: $font-20px; | ||||
|             } | ||||
| 
 | ||||
|             .mx_UserMenuButton_contextMenu_userId { | ||||
|                 font-size: $font-15px; | ||||
|                 line-height: $font-24px; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .mx_UserMenuButton_contextMenu_themeButton { | ||||
|             min-width: 32px; | ||||
|             max-width: 32px; | ||||
|             width: 32px; | ||||
|             height: 32px; | ||||
|             margin-left: 8px; | ||||
|             border-radius: 32px; | ||||
|             background-color: $theme-button-bg-color; | ||||
|             cursor: pointer; | ||||
| 
 | ||||
|             // to make alignment easier, create flexbox for the image | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_UserMenuButton_contextMenu_optionList { | ||||
|         margin-top: 20px; | ||||
| 
 | ||||
|         // This is a bit of a hack when we could just use a simple border-top property, | ||||
|         // however we have a (kinda) good reason for doing it this way: we need opacity. | ||||
|         // To get the right color, we need an opacity modifier which means we have to work | ||||
|         // around the problem. PostCSS doesn't support the opacity() function, and if we | ||||
|         // use something like postcss-functions we quickly run into an issue where the | ||||
|         // function we would define gets passed a CSS variable for custom themes, which | ||||
|         // can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379 | ||||
|         // | ||||
|         // Therefore, we just hack in a line and border the thing ourselves | ||||
|         &::before { | ||||
|             border-top: 1px solid $primary-fg-color; | ||||
|             opacity: 0.1; | ||||
|             content: ''; | ||||
| 
 | ||||
|             // Counteract the padding problems (width: 100% ignores the 40px padding, | ||||
|             // unless we position it absolutely then it does the right thing). | ||||
|             width: 100%; | ||||
|             position: absolute; | ||||
|             left: 0; | ||||
|         } | ||||
| 
 | ||||
|         ul { | ||||
|             list-style: none; | ||||
|             margin: 0; | ||||
|             padding: 0; | ||||
| 
 | ||||
|             li { | ||||
|                 margin: 0; | ||||
|                 padding: 20px 0 0; | ||||
| 
 | ||||
|                 .mx_AccessibleButton { | ||||
|                     text-decoration: none; | ||||
|                     color: $primary-fg-color; | ||||
|                     font-size: $font-15px; | ||||
|                     line-height: $font-24px; | ||||
| 
 | ||||
|                     // Create a flexbox to more easily define the list items | ||||
|                     display: flex; | ||||
|                     align-items: center; | ||||
| 
 | ||||
|                     img { // icons | ||||
|                         width: 16px; | ||||
|                         min-width: 16px; | ||||
|                         max-width: 16px; | ||||
|                     } | ||||
| 
 | ||||
|                     span { // labels | ||||
|                         padding-left: 14px; | ||||
|                         width: 100%; | ||||
|                         flex: 1; | ||||
| 
 | ||||
|                         // Ellipsize any text overflow | ||||
|                         text-overflow: ellipsis; | ||||
|                         overflow: hidden; | ||||
|                         white-space: nowrap; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,25 @@ | |||
| /* | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| // TODO: Rename to mx_RoomList during replacement of old component | ||||
| 
 | ||||
| .mx_RoomList2 { | ||||
|     // Create a column-based flexbox for the sublists. That's pretty much all we have to | ||||
|     // worry about in this stylesheet. | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex-wrap: wrap; | ||||
| } | ||||
|  | @ -14,8 +14,42 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| // TODO: Rename to mx_RoomSublist during replacement of old component | ||||
| 
 | ||||
| // TODO: Just use the 3 selectors we need from this instead of importing it. | ||||
| // We're going to end up with heavy modifications anyways. | ||||
| @import "../../../../node_modules/react-resizable/css/styles.css"; | ||||
| 
 | ||||
| .mx_RoomList2 .mx_RoomSubList_labelContainer { | ||||
|     z-index: 12; | ||||
| .mx_RoomSublist2 { | ||||
|     // The sublist is a column of rows, essentially | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| 
 | ||||
|     margin-left: 8px; | ||||
|     margin-top: 12px; | ||||
|     margin-bottom: 12px; | ||||
| 
 | ||||
|     .mx_RoomSublist2_headerContainer { | ||||
|         text-transform: uppercase; | ||||
|         opacity: 0.5; | ||||
|         line-height: $font-16px; | ||||
|         font-size: $font-12px; | ||||
|         padding-bottom: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_RoomSublist2_resizeBox { | ||||
|         // Create another flexbox column for the tiles | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         overflow: hidden; | ||||
| 
 | ||||
|         .mx_RoomSublist2_showMoreButton { | ||||
|             height: 44px; // 1 room tile high | ||||
|             cursor: pointer; | ||||
| 
 | ||||
|             // We create a flexbox to cheat at alignment | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,103 @@ | |||
| /* | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| // TODO: Rename to mx_RoomTile during replacement of old component | ||||
| 
 | ||||
| // Note: the room tile expects to be in a flexbox column container | ||||
| .mx_RoomTile2 { | ||||
|     width: calc(100% - 11px); // 8px for padding (4px on either side), 3px for margin | ||||
|     margin-bottom: 4px; | ||||
|     margin-right: 3px; | ||||
|     padding: 4px; | ||||
| 
 | ||||
|     // The tile is also a flexbox row itself | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
| 
 | ||||
|     &.mx_RoomTile2_selected { | ||||
|         background-color: $roomtile2-selected-bg-color; | ||||
|         border-radius: 32px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_RoomTile2_avatarContainer { | ||||
|         margin-right: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_RoomTile2_nameContainer { | ||||
|         // Create a new column layout flexbox for the name parts | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         justify-content: center; | ||||
| 
 | ||||
|         .mx_RoomTile2_name, | ||||
|         .mx_RoomTile2_messagePreview { | ||||
|             margin: 0 2px; | ||||
|         } | ||||
| 
 | ||||
|         // TODO: Ellipsis on the name and preview | ||||
| 
 | ||||
|         .mx_RoomTile2_name { | ||||
|             font-weight: 600; | ||||
|             font-size: $font-14px; | ||||
|             line-height: $font-19px; | ||||
|         } | ||||
| 
 | ||||
|         .mx_RoomTile2_messagePreview { | ||||
|             font-size: $font-13px; | ||||
|             line-height: $font-18px; | ||||
|             color: $roomtile2-preview-color; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_RoomTile2_badgeContainer { | ||||
|         flex-grow: 1; | ||||
| 
 | ||||
|         // Create another flexbox row because it's super easy to position the badge at | ||||
|         // the end this way. | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         justify-content: flex-end; | ||||
| 
 | ||||
|         .mx_RoomTile2_badge { | ||||
|             background-color: $roomtile2-badge-color; | ||||
| 
 | ||||
|             &:not(.mx_RoomTile2_badgeEmpty) { | ||||
|                 border-radius: 16px; | ||||
|                 font-size: $font-10px; | ||||
|                 line-height: $font-14px; | ||||
|                 text-align: center; | ||||
|                 font-weight: bold; | ||||
|                 margin-right: 14px; | ||||
|                 color: #fff; // TODO: Variable | ||||
| 
 | ||||
|                 // TODO: Confirm padding on counted badges | ||||
|                 padding: 2px 5px; | ||||
|             } | ||||
| 
 | ||||
|             &.mx_RoomTile2_badgeEmpty { | ||||
|                 width: 6px; | ||||
|                 height: 6px; | ||||
|                 border-radius: 6px; | ||||
|                 margin-right: 18px; | ||||
|             } | ||||
| 
 | ||||
|             &.mx_RoomTile2_badgeHighlight { | ||||
|                 // TODO: Use a more specific variable | ||||
|                 background-color: $warning-color; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg> | ||||
| After Width: | Height: | Size: 361 B | 
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-horizontal"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg> | ||||
| After Width: | Height: | Size: 343 B | 
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg> | ||||
| After Width: | Height: | Size: 650 B | 
|  | @ -172,6 +172,13 @@ $header-divider-color: #91A1C0; | |||
| 
 | ||||
| // ******************** | ||||
| 
 | ||||
| // TODO: Update variables for new room list | ||||
| // TODO: Dark theme | ||||
| $roomtile2-preview-color: #9e9e9e; | ||||
| $roomtile2-badge-color: #61708b; | ||||
| $roomtile2-selected-bg-color: #FFF; | ||||
| $theme-button-bg-color: #e3e8f0; | ||||
| 
 | ||||
| $roomtile-name-color: #61708b; | ||||
| $roomtile-badge-fg-color: $accent-fg-color; | ||||
| $roomtile-selected-color: #212121; | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ import RoomViewStore from './stores/RoomViewStore'; | |||
|  */ | ||||
| class ActiveRoomObserver { | ||||
|     constructor() { | ||||
|         this._listeners = {}; | ||||
|         this._listeners = {}; // key=roomId, value=function(isActive:boolean)
 | ||||
| 
 | ||||
|         this._activeRoomId = RoomViewStore.getRoomId(); | ||||
|         // TODO: We could self-destruct when the last listener goes away, or at least
 | ||||
|  | @ -35,6 +35,10 @@ class ActiveRoomObserver { | |||
|         this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); | ||||
|     } | ||||
| 
 | ||||
|     get activeRoomId(): string { | ||||
|         return this._activeRoomId; | ||||
|     } | ||||
| 
 | ||||
|     addListener(roomId, listener) { | ||||
|         if (!this._listeners[roomId]) this._listeners[roomId] = []; | ||||
|         this._listeners[roomId].push(listener); | ||||
|  | @ -51,23 +55,23 @@ class ActiveRoomObserver { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _emit(roomId) { | ||||
|     _emit(roomId, isActive: boolean) { | ||||
|         if (!this._listeners[roomId]) return; | ||||
| 
 | ||||
|         for (const l of this._listeners[roomId]) { | ||||
|             l.call(); | ||||
|             l.call(null, isActive); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _onRoomViewStoreUpdate() { | ||||
|         // emit for the old room ID
 | ||||
|         if (this._activeRoomId) this._emit(this._activeRoomId); | ||||
|         if (this._activeRoomId) this._emit(this._activeRoomId, false); | ||||
| 
 | ||||
|         // update our cache
 | ||||
|         this._activeRoomId = RoomViewStore.getRoomId(); | ||||
| 
 | ||||
|         // and emit for the new one
 | ||||
|         if (this._activeRoomId) this._emit(this._activeRoomId); | ||||
|         if (this._activeRoomId) this._emit(this._activeRoomId, true); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,6 +24,10 @@ import SearchBox from "./SearchBox"; | |||
| import RoomList2 from "../views/rooms/RoomList2"; | ||||
| import TopLeftMenuButton from "./TopLeftMenuButton"; | ||||
| import { Action } from "../../dispatcher/actions"; | ||||
| import { MatrixClientPeg } from "../../MatrixClientPeg"; | ||||
| import BaseAvatar from '../views/avatars/BaseAvatar'; | ||||
| import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; | ||||
| import UserMenuButton from "./UserMenuButton"; | ||||
| 
 | ||||
| /******************************************************************* | ||||
|  *   CAUTION                                                       * | ||||
|  | @ -46,7 +50,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { | |||
|     // TODO: Properly support TagPanel
 | ||||
|     // TODO: Properly support searching/filtering
 | ||||
|     // TODO: Properly support breadcrumbs
 | ||||
|     // TODO: Properly support TopLeftMenu (User Settings)
 | ||||
|     // TODO: a11y
 | ||||
|     // TODO: actually make this useful in general (match design proposals)
 | ||||
|     // TODO: Fadable support (is this still needed?)
 | ||||
|  | @ -82,24 +85,56 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private renderHeader(): React.ReactNode { | ||||
|         // TODO: Update when profile info changes
 | ||||
|         // TODO: Presence
 | ||||
|         // TODO: Breadcrumbs toggle
 | ||||
|         // TODO: Menu button
 | ||||
|         const avatarSize = 32; | ||||
|         // TODO: Don't do this profile lookup in render()
 | ||||
|         const client = MatrixClientPeg.get(); | ||||
|         let displayName = client.getUserId(); | ||||
|         let avatarUrl: string = null; | ||||
|         const myUser = client.getUser(client.getUserId()); | ||||
|         if (myUser) { | ||||
|             displayName = myUser.rawDisplayName; | ||||
|             avatarUrl = myUser.avatarUrl; | ||||
|         } | ||||
|         return ( | ||||
|             <div className="mx_LeftPanel2_userHeader"> | ||||
|                 <div className="mx_LeftPanel2_headerRow"> | ||||
|                     <span className="mx_LeftPanel2_userAvatarContainer"> | ||||
|                         <BaseAvatar | ||||
|                             idName={MatrixClientPeg.get().getUserId()} | ||||
|                             name={displayName} | ||||
|                             url={avatarUrl} | ||||
|                             width={avatarSize} | ||||
|                             height={avatarSize} | ||||
|                             resizeMethod="crop" | ||||
|                             className="mx_LeftPanel2_userAvatar" | ||||
|                         /> | ||||
|                     </span> | ||||
|                     <span className="mx_LeftPanel2_userName">{displayName}</span> | ||||
|                     <span className="mx_LeftPanel2_headerButtons"> | ||||
|                         <UserMenuButton /> | ||||
|                     </span> | ||||
|                 </div> | ||||
|                 <div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer"> | ||||
|                     <RoomBreadcrumbs /> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public render(): React.ReactNode { | ||||
|         const tagPanel = ( | ||||
|             <div className="mx_LeftPanel_tagPanelContainer"> | ||||
|             <div className="mx_LeftPanel2_tagPanelContainer"> | ||||
|                 <TagPanel/> | ||||
|             </div> | ||||
|         ); | ||||
| 
 | ||||
|         const exploreButton = ( | ||||
|             <div | ||||
|                 className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}> | ||||
|                 <AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}> | ||||
|                     {_t("Explore")} | ||||
|                 </AccessibleButton> | ||||
|             </div> | ||||
|         ); | ||||
| 
 | ||||
|         const searchBox = (<SearchBox | ||||
|             className="mx_LeftPanel_filterRooms" | ||||
|             className="mx_LeftPanel2_filterRoomsSearch" | ||||
|             enableRoomSearchFocus={true} | ||||
|             blurredPlaceholder={_t('Filter')} | ||||
|             placeholder={_t('Filter rooms…')} | ||||
|  | @ -124,29 +159,25 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { | |||
|         // TODO: Conference handling / calls
 | ||||
| 
 | ||||
|         const containerClasses = classNames({ | ||||
|             "mx_LeftPanel_container": true, | ||||
|             "mx_fadable": true, | ||||
|             "collapsed": false, // TODO: Collapsed support
 | ||||
|             "mx_LeftPanel_container_hasTagPanel": true, // TODO: TagPanel support
 | ||||
|             "mx_fadable_faded": false, | ||||
|             "mx_LeftPanel2": true, // TODO: Remove flag when RoomList2 ships (used as an indicator)
 | ||||
|             "mx_LeftPanel2": true, | ||||
|         }); | ||||
| 
 | ||||
|         return ( | ||||
|             <div className={containerClasses}> | ||||
|                 {tagPanel} | ||||
|                 <aside className="mx_LeftPanel dark-panel"> | ||||
|                     <TopLeftMenuButton collapsed={false}/> | ||||
|                 <aside className="mx_LeftPanel2_roomListContainer"> | ||||
|                     {this.renderHeader()} | ||||
|                     <div | ||||
|                         className="mx_LeftPanel_exploreAndFilterRow" | ||||
|                         className="mx_LeftPanel2_filterContainer" | ||||
|                         onKeyDown={() => {/*TODO*/}} | ||||
|                         onFocus={() => {/*TODO*/}} | ||||
|                         onBlur={() => {/*TODO*/}} | ||||
|                     > | ||||
|                         {exploreButton} | ||||
|                         {searchBox} | ||||
|                     </div> | ||||
|                     {roomList} | ||||
|                     <div className="mx_LeftPanel2_actualRoomListContainer"> | ||||
|                         {roomList} | ||||
|                     </div> | ||||
|                 </aside> | ||||
|             </div> | ||||
|         ); | ||||
|  |  | |||
|  | @ -452,9 +452,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> { | |||
|                 // composer, so CTRL+` it is
 | ||||
| 
 | ||||
|                 if (ctrlCmdOnly) { | ||||
|                     dis.dispatch({ | ||||
|                         action: 'toggle_top_left_menu', | ||||
|                     }); | ||||
|                     dis.fire(Action.ToggleUserMenu); | ||||
|                     handled = true; | ||||
|                 } | ||||
|                 break; | ||||
|  |  | |||
|  | @ -72,6 +72,7 @@ import { | |||
|     hideToast as hideAnalyticsToast | ||||
| } from "../../toasts/AnalyticsToast"; | ||||
| import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; | ||||
| import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; | ||||
| 
 | ||||
| /** constants for MatrixChat.state.view */ | ||||
| export enum Views { | ||||
|  | @ -604,9 +605,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|                 this.viewIndexedRoom(payload.roomIndex); | ||||
|                 break; | ||||
|             case Action.ViewUserSettings: { | ||||
|                 const tabPayload = payload as OpenToTabPayload; | ||||
|                 const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); | ||||
|                 Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, | ||||
|                     /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); | ||||
|                 Modal.createTrackedDialog('User settings', '', UserSettingsDialog, | ||||
|                     {initialTabId: tabPayload.initialTabId}, | ||||
|                     /*className=*/null, /*isPriority=*/false, /*isStatic=*/true | ||||
|                 ); | ||||
| 
 | ||||
|                 // View the welcome or home page if we need something to look at
 | ||||
|                 this.viewSomethingBehindModal(); | ||||
|  |  | |||
|  | @ -27,25 +27,20 @@ import { ReactNode } from "react"; | |||
|  * Represents a tab for the TabbedView. | ||||
|  */ | ||||
| export class Tab { | ||||
|     public label: string; | ||||
|     public icon: string; | ||||
|     public body: React.ReactNode; | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a new tab. | ||||
|      * @param {string} tabLabel The untranslated tab label. | ||||
|      * @param {string} tabIconClass The class for the tab icon. This should be a simple mask. | ||||
|      * @param {React.ReactNode} tabJsx The JSX for the tab container. | ||||
|      * @param {string} id The tab's ID. | ||||
|      * @param {string} label The untranslated tab label. | ||||
|      * @param {string} icon The class for the tab icon. This should be a simple mask. | ||||
|      * @param {React.ReactNode} body The JSX for the tab container. | ||||
|      */ | ||||
|     constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) { | ||||
|         this.label = tabLabel; | ||||
|         this.icon = tabIconClass; | ||||
|         this.body = tabJsx; | ||||
|     constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| interface IProps { | ||||
|     tabs: Tab[]; | ||||
|     initialTabId?: string; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|  | @ -53,16 +48,17 @@ interface IState { | |||
| } | ||||
| 
 | ||||
| export default class TabbedView extends React.Component<IProps, IState> { | ||||
|     static propTypes = { | ||||
|         // The tabs to show
 | ||||
|         tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired, | ||||
|     }; | ||||
| 
 | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         let activeTabIndex = 0; | ||||
|         if (props.initialTabId) { | ||||
|             const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId); | ||||
|             if (tabIndex >= 0) activeTabIndex = tabIndex; | ||||
|         } | ||||
| 
 | ||||
|         this.state = { | ||||
|             activeTabIndex: 0, | ||||
|             activeTabIndex, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ import * as Avatar from '../../Avatar'; | |||
| import { _t } from '../../languageHandler'; | ||||
| import dis from "../../dispatcher/dispatcher"; | ||||
| import {ContextMenu, ContextMenuButton} from "./ContextMenu"; | ||||
| import {Action} from "../../dispatcher/actions"; | ||||
| 
 | ||||
| const AVATAR_SIZE = 28; | ||||
| 
 | ||||
|  | @ -75,7 +76,7 @@ export default class TopLeftMenuButton extends React.Component { | |||
| 
 | ||||
|     onAction = (payload) => { | ||||
|         // For accessibility
 | ||||
|         if (payload.action === "toggle_top_left_menu") { | ||||
|         if (payload.action === Action.ToggleUserMenu) { | ||||
|             if (this._buttonRef) this._buttonRef.click(); | ||||
|         } | ||||
|     }; | ||||
|  |  | |||
|  | @ -0,0 +1,270 @@ | |||
| /* | ||||
| 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 * as React from "react"; | ||||
| import {User} from "matrix-js-sdk/src/models/user"; | ||||
| import { MatrixClientPeg } from "../../MatrixClientPeg"; | ||||
| import defaultDispatcher from "../../dispatcher/dispatcher"; | ||||
| import { ActionPayload } from "../../dispatcher/payloads"; | ||||
| import { Action } from "../../dispatcher/actions"; | ||||
| import { createRef } from "react"; | ||||
| import { _t } from "../../languageHandler"; | ||||
| import {ContextMenu, ContextMenuButton} from "./ContextMenu"; | ||||
| import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; | ||||
| import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; | ||||
| import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; | ||||
| import Modal from "../../Modal"; | ||||
| import LogoutDialog from "../views/dialogs/LogoutDialog"; | ||||
| import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; | ||||
| import {getCustomTheme} from "../../theme"; | ||||
| import {getHostingLink} from "../../utils/HostingLink"; | ||||
| import AccessibleButton from "../views/elements/AccessibleButton"; | ||||
| 
 | ||||
| interface IProps { | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     user: User; | ||||
|     menuDisplayed: boolean; | ||||
|     isDarkTheme: boolean; | ||||
| } | ||||
| 
 | ||||
| export default class UserMenuButton extends React.Component<IProps, IState> { | ||||
|     private dispatcherRef: string; | ||||
|     private themeWatcherRef: string; | ||||
|     private buttonRef: React.RefObject<HTMLButtonElement> = createRef(); | ||||
| 
 | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             menuDisplayed: false, | ||||
|             user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), | ||||
|             isDarkTheme: this.isUserOnDarkTheme(), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private get displayName(): string { | ||||
|         if (MatrixClientPeg.get().isGuest()) { | ||||
|             return _t("Guest"); | ||||
|         } else if (this.state.user) { | ||||
|             return this.state.user.displayName; | ||||
|         } else { | ||||
|             return MatrixClientPeg.get().getUserId(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public componentDidMount() { | ||||
|         this.dispatcherRef = defaultDispatcher.register(this.onAction); | ||||
|         this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); | ||||
|     } | ||||
| 
 | ||||
|     public componentWillUnmount() { | ||||
|         if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); | ||||
|         if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); | ||||
|     } | ||||
| 
 | ||||
|     private isUserOnDarkTheme(): boolean { | ||||
|         const theme = SettingsStore.getValue("theme"); | ||||
|         if (theme.startsWith("custom-")) { | ||||
|             return getCustomTheme(theme.substring(0, 7)).is_dark; | ||||
|         } | ||||
|         return theme === "dark"; | ||||
|     } | ||||
| 
 | ||||
|     private onThemeChanged = () => { | ||||
|         this.setState({isDarkTheme: this.isUserOnDarkTheme()}); | ||||
|     }; | ||||
| 
 | ||||
|     private onAction = (ev: ActionPayload) => { | ||||
|         if (ev.action !== Action.ToggleUserMenu) return; // not interested
 | ||||
| 
 | ||||
|         // For accessibility
 | ||||
|         if (this.buttonRef.current) this.buttonRef.current.click(); | ||||
|     }; | ||||
| 
 | ||||
|     private onOpenMenuClick = (ev: InputEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({menuDisplayed: true}); | ||||
|     }; | ||||
| 
 | ||||
|     private onCloseMenu = () => { | ||||
|         this.setState({menuDisplayed: false}); | ||||
|     }; | ||||
| 
 | ||||
|     private onSwitchThemeClick = () => { | ||||
|         // Disable system theme matching if the user hits this button
 | ||||
|         SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); | ||||
| 
 | ||||
|         const newTheme = this.state.isDarkTheme ? "light" : "dark"; | ||||
|         SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); | ||||
|     }; | ||||
| 
 | ||||
|     private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
| 
 | ||||
|         const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; | ||||
|         defaultDispatcher.dispatch(payload); | ||||
|         this.setState({menuDisplayed: false}); // also close the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     private onShowArchived = (ev: React.MouseEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
| 
 | ||||
|         // TODO: Archived room view (deferred)
 | ||||
|         console.log("TODO: Show archived rooms"); | ||||
|     }; | ||||
| 
 | ||||
|     private onProvideFeedback = (ev: React.MouseEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
| 
 | ||||
|         Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); | ||||
|         this.setState({menuDisplayed: false}); // also close the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     private onSignOutClick = (ev: React.MouseEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
| 
 | ||||
|         Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); | ||||
|         this.setState({menuDisplayed: false}); // also close the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     public render() { | ||||
|         let contextMenu; | ||||
|         if (this.state.menuDisplayed) { | ||||
|             let hostingLink; | ||||
|             const signupLink = getHostingLink("user-context-menu"); | ||||
|             if (signupLink) { | ||||
|                 hostingLink = ( | ||||
|                     <div className="mx_UserMenuButton_contextMenu_header"> | ||||
|                         {_t( | ||||
|                             "<a>Upgrade</a> to your own domain", {}, | ||||
|                             { | ||||
|                                 a: sub => ( | ||||
|                                     <a | ||||
|                                         href={signupLink} | ||||
|                                         target="_blank" | ||||
|                                         rel="noreferrer noopener" | ||||
|                                         tabIndex={-1} | ||||
|                                     >{sub}</a> | ||||
|                                 ), | ||||
|                             }, | ||||
|                         )} | ||||
|                     </div> | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             const elementRect = this.buttonRef.current.getBoundingClientRect(); | ||||
|             contextMenu = ( | ||||
|                 <ContextMenu | ||||
|                     chevronFace="none" | ||||
|                     left={elementRect.left} | ||||
|                     top={elementRect.top + elementRect.height} | ||||
|                     onFinished={this.onCloseMenu} | ||||
|                 > | ||||
|                     <div className="mx_UserMenuButton_contextMenu"> | ||||
|                         <div className="mx_UserMenuButton_contextMenu_header"> | ||||
|                             <div className="mx_UserMenuButton_contextMenu_name"> | ||||
|                                 <span className="mx_UserMenuButton_contextMenu_displayName"> | ||||
|                                     {this.displayName} | ||||
|                                 </span> | ||||
|                                 <span className="mx_UserMenuButton_contextMenu_userId"> | ||||
|                                     {MatrixClientPeg.get().getUserId()} | ||||
|                                 </span> | ||||
|                             </div> | ||||
|                             <div | ||||
|                                 className="mx_UserMenuButton_contextMenu_themeButton" | ||||
|                                 onClick={this.onSwitchThemeClick} | ||||
|                                 title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")} | ||||
|                             > | ||||
|                                 <img | ||||
|                                     src={require("../../../res/img/feather-customised/sun.svg")} | ||||
|                                     alt={_t("Switch theme")} | ||||
|                                     width={16} | ||||
|                                 /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         {hostingLink} | ||||
|                         <div className="mx_UserMenuButton_contextMenu_optionList"> | ||||
|                             <ul> | ||||
|                                 <li> | ||||
|                                     <AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> | ||||
|                                         <img src={require("../../../res/img/feather-customised/notifications.svg")} width={16} /> | ||||
|                                         <span>{_t("Notification settings")}</span> | ||||
|                                     </AccessibleButton> | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}> | ||||
|                                         <img src={require("../../../res/img/feather-customised/lock.svg")} width={16} /> | ||||
|                                         <span>{_t("Security & privacy")}</span> | ||||
|                                     </AccessibleButton> | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}> | ||||
|                                         <img src={require("../../../res/img/feather-customised/settings.svg")} width={16} /> | ||||
|                                         <span>{_t("All settings")}</span> | ||||
|                                     </AccessibleButton> | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <AccessibleButton onClick={this.onShowArchived}> | ||||
|                                         <img src={require("../../../res/img/feather-customised/archive.svg")} width={16} /> | ||||
|                                         <span>{_t("Archived rooms")}</span> | ||||
|                                     </AccessibleButton> | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <AccessibleButton onClick={this.onProvideFeedback}> | ||||
|                                         <img src={require("../../../res/img/feather-customised/message-circle.svg")} width={16} /> | ||||
|                                         <span>{_t("Feedback")}</span> | ||||
|                                     </AccessibleButton> | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                         <div className="mx_UserMenuButton_contextMenu_optionList"> | ||||
|                             <ul> | ||||
|                                 <li> | ||||
|                                     <AccessibleButton onClick={this.onSignOutClick}> | ||||
|                                         <img src={require("../../../res/img/feather-customised/sign-out.svg")} width={16} /> | ||||
|                                         <span>{_t("Sign out")}</span> | ||||
|                                     </AccessibleButton> | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </ContextMenu> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <React.Fragment> | ||||
|                 <ContextMenuButton | ||||
|                     className="mx_UserMenuButton" | ||||
|                     onClick={this.onOpenMenuClick} | ||||
|                     inputRef={this.buttonRef} | ||||
|                     label={_t("Account settings")} | ||||
|                     isExpanded={this.state.menuDisplayed} | ||||
|                 > | ||||
|                     <img src={require("../../../res/img/feather-customised/more-horizontal.svg")} alt="..." width={14} /> | ||||
|                 </ContextMenuButton> | ||||
|                 {contextMenu} | ||||
|             </React.Fragment> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -30,6 +30,13 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; | |||
| import dis from "../../../dispatcher/dispatcher"; | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| 
 | ||||
| export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; | ||||
| export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; | ||||
| export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; | ||||
| export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; | ||||
| export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; | ||||
| export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; | ||||
| 
 | ||||
| export default class RoomSettingsDialog extends React.Component { | ||||
|     static propTypes = { | ||||
|         roomId: PropTypes.string.isRequired, | ||||
|  | @ -56,21 +63,25 @@ export default class RoomSettingsDialog extends React.Component { | |||
|         const tabs = []; | ||||
| 
 | ||||
|         tabs.push(new Tab( | ||||
|             ROOM_GENERAL_TAB, | ||||
|             _td("General"), | ||||
|             "mx_RoomSettingsDialog_settingsIcon", | ||||
|             <GeneralRoomSettingsTab roomId={this.props.roomId} />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             ROOM_SECURITY_TAB, | ||||
|             _td("Security & Privacy"), | ||||
|             "mx_RoomSettingsDialog_securityIcon", | ||||
|             <SecurityRoomSettingsTab roomId={this.props.roomId} />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             ROOM_ROLES_TAB, | ||||
|             _td("Roles & Permissions"), | ||||
|             "mx_RoomSettingsDialog_rolesIcon", | ||||
|             <RolesRoomSettingsTab roomId={this.props.roomId} />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             ROOM_NOTIFICATIONS_TAB, | ||||
|             _td("Notifications"), | ||||
|             "mx_RoomSettingsDialog_notificationsIcon", | ||||
|             <NotificationSettingsTab roomId={this.props.roomId} />, | ||||
|  | @ -78,6 +89,7 @@ export default class RoomSettingsDialog extends React.Component { | |||
| 
 | ||||
|         if (SettingsStore.isFeatureEnabled("feature_bridge_state")) { | ||||
|             tabs.push(new Tab( | ||||
|                 ROOM_BRIDGES_TAB, | ||||
|                 _td("Bridges"), | ||||
|                 "mx_RoomSettingsDialog_bridgesIcon", | ||||
|                 <BridgeSettingsTab roomId={this.props.roomId} />, | ||||
|  | @ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component { | |||
|         } | ||||
| 
 | ||||
|         tabs.push(new Tab( | ||||
|             ROOM_ADVANCED_TAB, | ||||
|             _td("Advanced"), | ||||
|             "mx_RoomSettingsDialog_warningIcon", | ||||
|             <AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />, | ||||
|  |  | |||
|  | @ -33,9 +33,21 @@ import * as sdk from "../../../index"; | |||
| import SdkConfig from "../../../SdkConfig"; | ||||
| import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; | ||||
| 
 | ||||
| export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; | ||||
| export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; | ||||
| export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; | ||||
| export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; | ||||
| export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; | ||||
| export const USER_VOICE_TAB = "USER_VOICE_TAB"; | ||||
| export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; | ||||
| export const USER_LABS_TAB = "USER_LABS_TAB"; | ||||
| export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; | ||||
| export const USER_HELP_TAB = "USER_HELP_TAB"; | ||||
| 
 | ||||
| export default class UserSettingsDialog extends React.Component { | ||||
|     static propTypes = { | ||||
|         onFinished: PropTypes.func.isRequired, | ||||
|         initialTabId: PropTypes.string, | ||||
|     }; | ||||
| 
 | ||||
|     constructor() { | ||||
|  | @ -63,42 +75,50 @@ export default class UserSettingsDialog extends React.Component { | |||
|         const tabs = []; | ||||
| 
 | ||||
|         tabs.push(new Tab( | ||||
|             USER_GENERAL_TAB, | ||||
|             _td("General"), | ||||
|             "mx_UserSettingsDialog_settingsIcon", | ||||
|             <GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             USER_APPEARANCE_TAB, | ||||
|             _td("Appearance"), | ||||
|             "mx_UserSettingsDialog_appearanceIcon", | ||||
|             <AppearanceUserSettingsTab />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             USER_FLAIR_TAB, | ||||
|             _td("Flair"), | ||||
|             "mx_UserSettingsDialog_flairIcon", | ||||
|             <FlairUserSettingsTab />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             USER_NOTIFICATIONS_TAB, | ||||
|             _td("Notifications"), | ||||
|             "mx_UserSettingsDialog_bellIcon", | ||||
|             <NotificationUserSettingsTab />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             USER_PREFERENCES_TAB, | ||||
|             _td("Preferences"), | ||||
|             "mx_UserSettingsDialog_preferencesIcon", | ||||
|             <PreferencesUserSettingsTab />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             USER_VOICE_TAB, | ||||
|             _td("Voice & Video"), | ||||
|             "mx_UserSettingsDialog_voiceIcon", | ||||
|             <VoiceUserSettingsTab />, | ||||
|         )); | ||||
|         tabs.push(new Tab( | ||||
|             USER_SECURITY_TAB, | ||||
|             _td("Security & Privacy"), | ||||
|             "mx_UserSettingsDialog_securityIcon", | ||||
|             <SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />, | ||||
|         )); | ||||
|         if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { | ||||
|             tabs.push(new Tab( | ||||
|                 USER_LABS_TAB, | ||||
|                 _td("Labs"), | ||||
|                 "mx_UserSettingsDialog_labsIcon", | ||||
|                 <LabsUserSettingsTab />, | ||||
|  | @ -106,12 +126,14 @@ export default class UserSettingsDialog extends React.Component { | |||
|         } | ||||
|         if (this.state.mjolnirEnabled) { | ||||
|             tabs.push(new Tab( | ||||
|                 USER_MJOLNIR_TAB, | ||||
|                 _td("Ignored users"), | ||||
|                 "mx_UserSettingsDialog_mjolnirIcon", | ||||
|                 <MjolnirUserSettingsTab />, | ||||
|             )); | ||||
|         } | ||||
|         tabs.push(new Tab( | ||||
|             USER_HELP_TAB, | ||||
|             _td("Help & About"), | ||||
|             "mx_UserSettingsDialog_helpIcon", | ||||
|             <HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />, | ||||
|  | @ -127,7 +149,7 @@ export default class UserSettingsDialog extends React.Component { | |||
|             <BaseDialog className='mx_UserSettingsDialog' hasCancel={true} | ||||
|                         onFinished={this.props.onFinished} title={_t("Settings")}> | ||||
|                 <div className='ms_SettingsDialog_content'> | ||||
|                     <TabbedView tabs={this._getTabs()} /> | ||||
|                     <TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} /> | ||||
|                 </div> | ||||
|             </BaseDialog> | ||||
|         ); | ||||
|  |  | |||
|  | @ -96,7 +96,7 @@ const TAG_AESTHETICS: { | |||
|         defaultHidden: false, | ||||
|     }, | ||||
|     [DefaultTagID.DM]: { | ||||
|         sectionLabel: _td("Direct Messages"), | ||||
|         sectionLabel: _td("People"), | ||||
|         isInvite: false, | ||||
|         defaultHidden: false, | ||||
|         addRoomLabel: _td("Start chat"), | ||||
|  | @ -200,6 +200,7 @@ export default class RoomList2 extends React.Component<IProps, IState> { | |||
|                     addRoomLabel={aesthetics.addRoomLabel} | ||||
|                     isInvite={aesthetics.isInvite} | ||||
|                     layout={this.state.layouts.get(orderedTagId)} | ||||
|                     showMessagePreviews={orderedTagId === DefaultTagID.DM} | ||||
|                 /> | ||||
|             ); | ||||
|         } | ||||
|  | @ -216,7 +217,7 @@ export default class RoomList2 extends React.Component<IProps, IState> { | |||
|                         onFocus={this.props.onFocus} | ||||
|                         onBlur={this.props.onBlur} | ||||
|                         onKeyDown={onKeyDownHandler} | ||||
|                         className="mx_RoomList mx_RoomList2" | ||||
|                         className="mx_RoomList2" | ||||
|                         role="tree" | ||||
|                         aria-label={_t("Rooms")} | ||||
|                         // Firefox sometimes makes this element focusable due to
 | ||||
|  |  | |||
|  | @ -20,15 +20,13 @@ import * as React from "react"; | |||
| import { createRef } from "react"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import classNames from 'classnames'; | ||||
| import * as RoomNotifs from '../../../RoomNotifs'; | ||||
| import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; | ||||
| import { _t } from "../../../languageHandler"; | ||||
| import AccessibleButton from "../../views/elements/AccessibleButton"; | ||||
| import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton"; | ||||
| import * as FormattingUtils from '../../../utils/FormattingUtils'; | ||||
| import RoomTile2 from "./RoomTile2"; | ||||
| import { ResizableBox, ResizeCallbackData } from "react-resizable"; | ||||
| import { ListLayout } from "../../../stores/room-list/ListLayout"; | ||||
| import { DefaultTagID, TagID } from "../../../stores/room-list/models"; | ||||
| 
 | ||||
| /******************************************************************* | ||||
|  *   CAUTION                                                       * | ||||
|  | @ -43,6 +41,7 @@ interface IProps { | |||
|     rooms?: Room[]; | ||||
|     startAsHidden: boolean; | ||||
|     label: string; | ||||
|     showMessagePreviews: boolean; | ||||
|     onAddRoom?: () => void; | ||||
|     addRoomLabel: string; | ||||
|     isInvite: boolean; | ||||
|  | @ -93,7 +92,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         if (this.props.rooms) { | ||||
|             for (const room of this.props.rooms) { | ||||
|                 tiles.push(<RoomTile2 room={room} key={`room-${room.roomId}`}/>); | ||||
|                 tiles.push( | ||||
|                     <RoomTile2 | ||||
|                         room={room} | ||||
|                         key={`room-${room.roomId}`} | ||||
|                         showMessagePreview={this.props.showMessagePreviews} | ||||
|                     /> | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -101,25 +106,16 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|     } | ||||
| 
 | ||||
|     private renderHeader(): React.ReactElement { | ||||
|         const notifications = !this.props.isInvite | ||||
|             ? RoomNotifs.aggregateNotificationCount(this.props.rooms) | ||||
|             : {count: 0, highlight: true}; | ||||
|         const notifCount = notifications.count; | ||||
|         const notifHighlight = notifications.highlight; | ||||
|         // TODO: Handle badge count
 | ||||
|         // const notifications = !this.props.isInvite
 | ||||
|         //     ? RoomNotifs.aggregateNotificationCount(this.props.rooms)
 | ||||
|         //     : {count: 0, highlight: true};
 | ||||
|         // const notifCount = notifications.count;
 | ||||
|         // const notifHighlight = notifications.highlight;
 | ||||
| 
 | ||||
|         // TODO: Title on collapsed
 | ||||
|         // TODO: Incoming call box
 | ||||
| 
 | ||||
|         let chevron = null; | ||||
|         if (this.hasTiles()) { | ||||
|             const chevronClasses = classNames({ | ||||
|                 'mx_RoomSubList_chevron': true, | ||||
|                 'mx_RoomSubList_chevronRight': false, // isCollapsed
 | ||||
|                 'mx_RoomSubList_chevronDown': true, // !isCollapsed
 | ||||
|             }); | ||||
|             chevron = (<div className={chevronClasses}/>); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <RovingTabIndexWrapper inputRef={this.headerButton}> | ||||
|                 {({onFocus, isActive, ref}) => { | ||||
|  | @ -127,68 +123,68 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|                     const tabIndex = isActive ? 0 : -1; | ||||
| 
 | ||||
|                     // TODO: Collapsed state
 | ||||
|                     let badge; | ||||
|                     if (true) { // !isCollapsed
 | ||||
|                         const badgeClasses = classNames({ | ||||
|                             'mx_RoomSubList_badge': true, | ||||
|                             'mx_RoomSubList_badgeHighlight': notifHighlight, | ||||
|                         }); | ||||
|                         // Wrap the contents in a div and apply styles to the child div so that the browser default outline works
 | ||||
|                         if (notifCount > 0) { | ||||
|                             badge = ( | ||||
|                                 <AccessibleButton | ||||
|                                     tabIndex={tabIndex} | ||||
|                                     className={badgeClasses} | ||||
|                                     aria-label={_t("Jump to first unread room.")} | ||||
|                                 > | ||||
|                                     <div> | ||||
|                                         {FormattingUtils.formatCount(notifCount)} | ||||
|                                     </div> | ||||
|                                 </AccessibleButton> | ||||
|                             ); | ||||
|                         } else if (this.props.isInvite && this.hasTiles()) { | ||||
|                             // Render the `!` badge for invites
 | ||||
|                             badge = ( | ||||
|                                 <AccessibleButton | ||||
|                                     tabIndex={tabIndex} | ||||
|                                     className={badgeClasses} | ||||
|                                     aria-label={_t("Jump to first invite.")} | ||||
|                                 > | ||||
|                                     <div> | ||||
|                                         {FormattingUtils.formatCount(this.numTiles)} | ||||
|                                     </div> | ||||
|                                 </AccessibleButton> | ||||
|                             ); | ||||
|                         } | ||||
|                     } | ||||
|                     // TODO: Handle badge count
 | ||||
|                     // let badge;
 | ||||
|                     // if (true) { // !isCollapsed
 | ||||
|                     //     const showCount = localStorage.getItem("mx_rls_count") || notifHighlight;
 | ||||
|                     //     const badgeClasses = classNames({
 | ||||
|                     //         'mx_RoomSublist2_badge': true,
 | ||||
|                     //         'mx_RoomSublist2_badgeHighlight': notifHighlight,
 | ||||
|                     //         'mx_RoomSublist2_badgeEmpty': !showCount,
 | ||||
|                     //     });
 | ||||
|                     //     // Wrap the contents in a div and apply styles to the child div so that the browser default outline works
 | ||||
|                     //     if (notifCount > 0) {
 | ||||
|                     //         const count = <div>{FormattingUtils.formatCount(notifCount)}</div>;
 | ||||
|                     //         badge = (
 | ||||
|                     //             <AccessibleButton
 | ||||
|                     //                 tabIndex={tabIndex}
 | ||||
|                     //                 className={badgeClasses}
 | ||||
|                     //                 aria-label={_t("Jump to first unread room.")}
 | ||||
|                     //             >
 | ||||
|                     //                 {showCount ? count : null}
 | ||||
|                     //             </AccessibleButton>
 | ||||
|                     //         );
 | ||||
|                     //     } else if (this.props.isInvite && this.hasTiles()) {
 | ||||
|                     //         // Render the `!` badge for invites
 | ||||
|                     //         badge = (
 | ||||
|                     //             <AccessibleButton
 | ||||
|                     //                 tabIndex={tabIndex}
 | ||||
|                     //                 className={badgeClasses}
 | ||||
|                     //                 aria-label={_t("Jump to first invite.")}
 | ||||
|                     //             >
 | ||||
|                     //                 <div>
 | ||||
|                     //                     {FormattingUtils.formatCount(this.numTiles)}
 | ||||
|                     //                 </div>
 | ||||
|                     //             </AccessibleButton>
 | ||||
|                     //         );
 | ||||
|                     //     }
 | ||||
|                     // }
 | ||||
| 
 | ||||
|                     let addRoomButton = null; | ||||
|                     if (!!this.props.onAddRoom) { | ||||
|                         addRoomButton = ( | ||||
|                             <AccessibleTooltipButton | ||||
|                                 tabIndex={tabIndex} | ||||
|                                 onClick={this.onAddRoom} | ||||
|                                 className="mx_RoomSubList_addRoom" | ||||
|                                 title={this.props.addRoomLabel || _t("Add room")} | ||||
|                             /> | ||||
|                         ); | ||||
|                     } | ||||
|                     // TODO: Aux button
 | ||||
|                     // let addRoomButton = null;
 | ||||
|                     // if (!!this.props.onAddRoom) {
 | ||||
|                     //     addRoomButton = (
 | ||||
|                     //         <AccessibleTooltipButton
 | ||||
|                     //             tabIndex={tabIndex}
 | ||||
|                     //             onClick={this.onAddRoom}
 | ||||
|                     //             className="mx_RoomSublist2_addButton"
 | ||||
|                     //             title={this.props.addRoomLabel || _t("Add room")}
 | ||||
|                     //         />
 | ||||
|                     //     );
 | ||||
|                     // }
 | ||||
| 
 | ||||
|                     // TODO: a11y (see old component)
 | ||||
|                     return ( | ||||
|                         <div className={"mx_RoomSubList_labelContainer"}> | ||||
|                         <div className={"mx_RoomSublist2_headerContainer"}> | ||||
|                             <AccessibleButton | ||||
|                                 inputRef={ref} | ||||
|                                 tabIndex={tabIndex} | ||||
|                                 className={"mx_RoomSubList_label"} | ||||
|                                 className={"mx_RoomSublist2_headerText"} | ||||
|                                 role="treeitem" | ||||
|                                 aria-level="1" | ||||
|                             > | ||||
|                                 {chevron} | ||||
|                                 <span>{this.props.label}</span> | ||||
|                             </AccessibleButton> | ||||
|                             {badge} | ||||
|                             {addRoomButton} | ||||
|                         </div> | ||||
|                     ); | ||||
|                 }} | ||||
|  | @ -204,9 +200,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         const classes = classNames({ | ||||
|             // TODO: Proper collapse support
 | ||||
|             'mx_RoomSubList': true, | ||||
|             'mx_RoomSubList_hidden': false, // len && isCollapsed
 | ||||
|             'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed
 | ||||
|             'mx_RoomSublist2': true, | ||||
|             'mx_RoomSublist2_collapsed': false, // len && isCollapsed
 | ||||
|         }); | ||||
| 
 | ||||
|         let content = null; | ||||
|  | @ -244,7 +239,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|                 visibleTiles.splice(visibleTiles.length - 1, 1, ( | ||||
|                     <div | ||||
|                         onClick={this.onShowAllClick} | ||||
|                         style={{height: '34px', lineHeight: '34px', cursor: 'pointer'}} | ||||
|                         className='mx_RoomSublist2_showMoreButton' | ||||
|                         key='showall' | ||||
|                     > | ||||
|                         {_t("Show %(n)s more", {n: numMissing})} | ||||
|  |  | |||
|  | @ -23,7 +23,6 @@ import classNames from "classnames"; | |||
| import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; | ||||
| import AccessibleButton from "../../views/elements/AccessibleButton"; | ||||
| import RoomAvatar from "../../views/avatars/RoomAvatar"; | ||||
| import Tooltip from "../../views/elements/Tooltip"; | ||||
| import dis from '../../../dispatcher/dispatcher'; | ||||
| import { Key } from "../../../Keyboard"; | ||||
| import * as RoomNotifs from '../../../RoomNotifs'; | ||||
|  | @ -32,6 +31,7 @@ import * as Unread from '../../../Unread'; | |||
| import * as FormattingUtils from "../../../utils/FormattingUtils"; | ||||
| import { MatrixClientPeg } from "../../../MatrixClientPeg"; | ||||
| import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | ||||
| import ActiveRoomObserver from "../../../ActiveRoomObserver"; | ||||
| 
 | ||||
| /******************************************************************* | ||||
|  *   CAUTION                                                       * | ||||
|  | @ -51,6 +51,7 @@ enum NotificationColor { | |||
| 
 | ||||
| interface IProps { | ||||
|     room: Room; | ||||
|     showMessagePreview: boolean; | ||||
| 
 | ||||
|     // TODO: Allow falsifying counts (for invites and stuff)
 | ||||
|     // TODO: Transparency? Was this ever used?
 | ||||
|  | @ -65,6 +66,7 @@ interface INotificationState { | |||
| interface IState { | ||||
|     hover: boolean; | ||||
|     notificationState: INotificationState; | ||||
|     selected: boolean; | ||||
| } | ||||
| 
 | ||||
| export default class RoomTile2 extends React.Component<IProps, IState> { | ||||
|  | @ -87,12 +89,14 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|         this.state = { | ||||
|             hover: false, | ||||
|             notificationState: this.getNotificationState(), | ||||
|             selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, | ||||
|         }; | ||||
| 
 | ||||
|         this.props.room.on("Room.receipt", this.handleRoomEventUpdate); | ||||
|         this.props.room.on("Room.timeline", this.handleRoomEventUpdate); | ||||
|         this.props.room.on("Room.redaction", this.handleRoomEventUpdate); | ||||
|         MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate); | ||||
|         ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); | ||||
|     } | ||||
| 
 | ||||
|     public componentWillUnmount() { | ||||
|  | @ -100,6 +104,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|             this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate); | ||||
|             this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate); | ||||
|             this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate); | ||||
|             ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); | ||||
|         } | ||||
|         if (MatrixClientPeg.get()) { | ||||
|             MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate); | ||||
|  | @ -186,39 +191,33 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     private onActiveRoomUpdate = (isActive: boolean) => { | ||||
|         this.setState({selected: isActive}); | ||||
|     }; | ||||
| 
 | ||||
|     public render(): React.ReactElement { | ||||
|         // TODO: Collapsed state
 | ||||
|         // TODO: Invites
 | ||||
|         // TODO: a11y proper
 | ||||
|         // TODO: Render more than bare minimum
 | ||||
| 
 | ||||
|         const hasBadge = this.state.notificationState.color > NotificationColor.Bold; | ||||
|         const isUnread = this.state.notificationState.color > NotificationColor.None; | ||||
|         const classes = classNames({ | ||||
|             'mx_RoomTile': true, | ||||
|             // 'mx_RoomTile_selected': this.state.selected,
 | ||||
|             'mx_RoomTile_unread': isUnread, | ||||
|             'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey, | ||||
|             'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red, | ||||
|             'mx_RoomTile_invited': this.roomIsInvite, | ||||
|             // 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
 | ||||
|             'mx_RoomTile_noBadges': !hasBadge, | ||||
|             // 'mx_RoomTile_transparent': this.props.transparent,
 | ||||
|             // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
 | ||||
|             'mx_RoomTile2': true, | ||||
|             'mx_RoomTile2_selected': this.state.selected, | ||||
|         }); | ||||
| 
 | ||||
|         const avatarClasses = classNames({ | ||||
|             'mx_RoomTile_avatar': true, | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         let badge; | ||||
|         const hasBadge = this.state.notificationState.color > NotificationColor.Bold; | ||||
|         if (hasBadge) { | ||||
|             const hasNotif = this.state.notificationState.color >= NotificationColor.Red; | ||||
|             const isEmptyBadge = !localStorage.getItem("mx_rl_rt_badgeCount"); | ||||
|             const badgeClasses = classNames({ | ||||
|                 'mx_RoomTile_badge': true, | ||||
|                 'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed
 | ||||
|                 'mx_RoomTile2_badge': true, | ||||
|                 'mx_RoomTile2_badgeHighlight': hasNotif, | ||||
|                 'mx_RoomTile2_badgeEmpty': isEmptyBadge, | ||||
|             }); | ||||
|             badge = <div className={badgeClasses}>{this.state.notificationState.symbol}</div>; | ||||
|             const symbol = this.state.notificationState.symbol; | ||||
|             badge = <div className={badgeClasses}>{isEmptyBadge ? null : symbol}</div>; | ||||
|         } | ||||
| 
 | ||||
|         // TODO: the original RoomTile uses state for the room name. Do we need to?
 | ||||
|  | @ -226,20 +225,21 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|         if (typeof name !== 'string') name = ''; | ||||
|         name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
 | ||||
| 
 | ||||
|         const nameClasses = classNames({ | ||||
|             'mx_RoomTile_name': true, | ||||
|             'mx_RoomTile_invite': this.roomIsInvite, | ||||
|             'mx_RoomTile_badgeShown': hasBadge, | ||||
|         }); | ||||
| 
 | ||||
|         // TODO: Support collapsed state properly
 | ||||
|         let tooltip = null; | ||||
|         if (false) { // isCollapsed
 | ||||
|             if (this.state.hover) { | ||||
|                 tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} /> | ||||
|             } | ||||
|         // TODO: Tooltip?
 | ||||
| 
 | ||||
|         let messagePreview = null; | ||||
|         if (this.props.showMessagePreview) { | ||||
|             // TODO: Actually get the real message preview from state
 | ||||
|             messagePreview = <div className="mx_RoomTile2_messagePreview">I just ate a pie.</div>; | ||||
|         } | ||||
| 
 | ||||
|         const nameClasses = classNames({ | ||||
|             "mx_RoomTile2_name": true, | ||||
|             "mx_RoomTile2_nameWithPreview": !!messagePreview, | ||||
|         }); | ||||
| 
 | ||||
|         const avatarSize = 32; | ||||
|         return ( | ||||
|             <React.Fragment> | ||||
|                 <RovingTabIndexWrapper inputRef={this.roomTile}> | ||||
|  | @ -254,20 +254,18 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|                             onClick={this.onTileClick} | ||||
|                             role="treeitem" | ||||
|                         > | ||||
|                             <div className={avatarClasses}> | ||||
|                                 <div className="mx_RoomTile_avatar_container"> | ||||
|                                     <RoomAvatar room={this.props.room} width={24} height={24}/> | ||||
|                                 </div> | ||||
|                             <div className="mx_RoomTile2_avatarContainer"> | ||||
|                                 <RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/> | ||||
|                             </div> | ||||
|                             <div className="mx_RoomTile_nameContainer"> | ||||
|                                 <div className="mx_RoomTile_labelContainer"> | ||||
|                                     <div title={name} className={nameClasses} tabIndex={-1} dir="auto"> | ||||
|                                         {name} | ||||
|                                     </div> | ||||
|                             <div className="mx_RoomTile2_nameContainer"> | ||||
|                                 <div title={name} className={nameClasses} tabIndex={-1} dir="auto"> | ||||
|                                     {name} | ||||
|                                 </div> | ||||
|                                 {messagePreview} | ||||
|                             </div> | ||||
|                             <div className="mx_RoomTile2_badgeContainer"> | ||||
|                                 {badge} | ||||
|                             </div> | ||||
|                             {tooltip} | ||||
|                         </AccessibleButton> | ||||
|                     } | ||||
|                 </RovingTabIndexWrapper> | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ export enum Action { | |||
| 
 | ||||
|     /** | ||||
|      * Open the user settings. No additional payload information required. | ||||
|      * Optionally can include an OpenToTabPayload. | ||||
|      */ | ||||
|     ViewUserSettings = "view_user_settings", | ||||
| 
 | ||||
|  | @ -58,4 +59,9 @@ export enum Action { | |||
|      * Focuses the user's cursor to the composer. No additional payload information required. | ||||
|      */ | ||||
|     FocusComposer = "focus_composer", | ||||
| 
 | ||||
|     /** | ||||
|      * Opens the user menu (previously known as the top left menu). No additional payload information required. | ||||
|      */ | ||||
|     ToggleUserMenu = "toggle_user_menu", | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,27 @@ | |||
| /* | ||||
| 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 { ActionPayload } from "../payloads"; | ||||
| import { Action } from "../actions"; | ||||
| 
 | ||||
| export interface OpenToTabPayload extends ActionPayload { | ||||
|     action: Action.ViewUserSettings | string, // TODO: Add room settings action
 | ||||
| 
 | ||||
|     /** | ||||
|      * The tab ID to open in the settings view to start, if possible. | ||||
|      */ | ||||
|     initialTabId?: string; | ||||
| } | ||||
|  | @ -1090,6 +1090,7 @@ | |||
|     "Low priority": "Low priority", | ||||
|     "Historical": "Historical", | ||||
|     "System Alerts": "System Alerts", | ||||
|     "People": "People", | ||||
|     "This room": "This room", | ||||
|     "Joining room …": "Joining room …", | ||||
|     "Loading …": "Loading …", | ||||
|  | @ -1133,9 +1134,6 @@ | |||
|     "Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>", | ||||
|     "Not now": "Not now", | ||||
|     "Don't ask me again": "Don't ask me again", | ||||
|     "Jump to first unread room.": "Jump to first unread room.", | ||||
|     "Jump to first invite.": "Jump to first invite.", | ||||
|     "Add room": "Add room", | ||||
|     "Show %(n)s more": "Show %(n)s more", | ||||
|     "Options": "Options", | ||||
|     "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", | ||||
|  | @ -2017,6 +2015,9 @@ | |||
|     "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", | ||||
|     "Active call": "Active call", | ||||
|     "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?", | ||||
|     "Jump to first unread room.": "Jump to first unread room.", | ||||
|     "Jump to first invite.": "Jump to first invite.", | ||||
|     "Add room": "Add room", | ||||
|     "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", | ||||
|     "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", | ||||
|     "Search failed": "Search failed", | ||||
|  | @ -2041,6 +2042,14 @@ | |||
|     "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", | ||||
|     "Switch to light mode": "Switch to light mode", | ||||
|     "Switch to dark mode": "Switch to dark mode", | ||||
|     "Switch theme": "Switch theme", | ||||
|     "Security & privacy": "Security & privacy", | ||||
|     "All settings": "All settings", | ||||
|     "Archived rooms": "Archived rooms", | ||||
|     "Feedback": "Feedback", | ||||
|     "Account settings": "Account settings", | ||||
|     "Could not load user profile": "Could not load user profile", | ||||
|     "Verify this login": "Verify this login", | ||||
|     "Session verified": "Session verified", | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ limitations under the License. | |||
| 
 | ||||
| import { TagID } from "./models"; | ||||
| 
 | ||||
| const TILE_HEIGHT_PX = 34; | ||||
| const TILE_HEIGHT_PX = 44; | ||||
| 
 | ||||
| interface ISerializedListLayout { | ||||
|     numTiles: number; | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ function setCustomThemeVars(customTheme) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| function getCustomTheme(themeName) { | ||||
| export function getCustomTheme(themeName) { | ||||
|     // set css variables
 | ||||
|     const customThemes = SettingsStore.getValue("custom_themes"); | ||||
|     if (!customThemes) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston